diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index bece1681..bd4197e2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -923,6 +923,163 @@ namespace MobileGL::MG_Backend::DirectVulkan { ProgramFactory::CompileOptionFlags m_transformFlags; }; + // Decorates the module's captured varyings for VK_EXT_transform_feedback: + // user outputs get XfbBuffer/XfbStride/Offset directly; a captured + // gl_Position (a gl_PerVertex member) is mirrored into a dedicated output + // variable copied before every OpReturn, BEFORE the position fixup runs, + // so the captured value is the shader's own (pre-remap) gl_Position. + class XfbCaptureDecoratePass final : public spvtools::opt::Pass { + public: + struct CapturedVarying { + std::string name; + Uint32 bufferIndex = 0; + Uint32 offsetBytes = 0; + }; + const char* name() const override { return "mobilegl-xfb-capture-decorate"; } + XfbCaptureDecoratePass(Vector varyings, Vector strides) + : m_varyings(Move(varyings)), m_strides(Move(strides)) {} + + Status Process() override { + using namespace spvtools::opt; + if (m_varyings.empty()) return Status::SuccessWithoutChange; + + auto entryPointIter = get_module()->entry_points().begin(); + if (entryPointIter == get_module()->entry_points().end()) return Status::SuccessWithoutChange; + spvtools::opt::Instruction* entryPoint = &*entryPointIter; + const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1); + + // Name -> result id map from the debug section. + std::unordered_map idsByName; + for (auto& debugInst : get_module()->debugs2()) { + if (debugInst.opcode() != spv::Op::OpName) continue; + idsByName[debugInst.GetInOperand(1).AsString()] = debugInst.GetSingleWordInOperand(0); + } + + auto* decorationManager = context()->get_decoration_mgr(); + const auto decorateForXfb = [&](Uint32 targetId, Uint32 bufferIndex, Uint32 offsetBytes) { + const Uint32 stride = bufferIndex < m_strides.size() ? m_strides[bufferIndex] : 0; + decorationManager->AddDecorationVal(targetId, static_cast(spv::Decoration::XfbBuffer), + bufferIndex); + decorationManager->AddDecorationVal(targetId, static_cast(spv::Decoration::XfbStride), + stride); + decorationManager->AddDecorationVal(targetId, static_cast(spv::Decoration::Offset), + offsetBytes); + }; + + Bool modified = false; + Bool needsPositionMirror = false; + Uint32 positionBufferIndex = 0; + Uint32 positionOffset = 0; + for (const auto& varying : m_varyings) { + if (varying.name == "gl_Position") { + needsPositionMirror = true; + positionBufferIndex = varying.bufferIndex; + positionOffset = varying.offsetBytes; + continue; + } + const auto idIt = idsByName.find(varying.name); + if (idIt == idsByName.end()) { + MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str()); + continue; + } + decorateForXfb(idIt->second, varying.bufferIndex, varying.offsetBytes); + modified = true; + } + + if (needsPositionMirror) { + modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex, + positionOffset, decorateForXfb); + } + + if (!modified) return Status::SuccessWithoutChange; + + context()->AddCapability(spv::Capability::TransformFeedback); + { + auto executionMode = MakeUnique( + context(), spv::Op::OpExecutionMode, 0, 0, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {entryPoint->GetSingleWordInOperand(1)}}, + {SPV_OPERAND_TYPE_EXECUTION_MODE, {static_cast(spv::ExecutionMode::Xfb)}}}); + get_module()->AddExecutionMode(Move(executionMode)); + } + context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + private: + template + Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint, + Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) { + const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0); + using namespace spvtools::opt; + PositionTargetInfo target{}; + if (!FindPositionTarget(context(), &target)) { + MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found"); + return false; + } + if (!target.isMember) { + // Standalone gl_Position variable: decorate it directly. + decorateForXfb(target.variableId, bufferIndex, offsetBytes); + return true; + } + + auto* typeManager = context()->get_type_mgr(); + const Uint32 mirrorPointerTypeId = + typeManager->FindPointerToType(target.vectorTypeId, spv::StorageClass::Output); + if (mirrorPointerTypeId == 0) return false; + + const Uint32 mirrorVariableId = context()->TakeNextId(); + auto mirrorVariable = MakeUnique( + context(), spv::Op::OpVariable, mirrorPointerTypeId, mirrorVariableId, + std::initializer_list{ + {SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast(spv::StorageClass::Output)}}}); + get_module()->AddGlobalValue(Move(mirrorVariable)); + + // A free output location: past every explicitly decorated output. + Uint32 mirrorLocation = 0; + for (auto& annotation : get_module()->annotations()) { + if (annotation.opcode() != spv::Op::OpDecorate || + annotation.GetSingleWordInOperand(1) != static_cast(spv::Decoration::Location)) { + continue; + } + mirrorLocation = std::max(mirrorLocation, annotation.GetSingleWordInOperand(2) + 1); + } + auto* decorationManager = context()->get_decoration_mgr(); + decorationManager->AddDecorationVal(mirrorVariableId, + static_cast(spv::Decoration::Location), mirrorLocation); + decorateForXfb(mirrorVariableId, bufferIndex, offsetBytes); + entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {mirrorVariableId}}); + + auto* function = context()->GetFunction(entryFunctionId); + if (function == nullptr) return false; + const auto model = static_cast(entryPointModel); + Bool injected = false; + for (auto& block : *function) { + for (auto instIter = block.begin(); instIter != block.end(); ++instIter) { + // Geometry stages capture per emitted vertex; other stages at return. + const Bool isInjectionSite = + model == spv::ExecutionModel::Geometry + ? instIter->opcode() == spv::Op::OpEmitVertex + : instIter->opcode() == spv::Op::OpReturn; + if (!isInjectionSite) continue; + InstructionBuilder builder(context(), &*instIter, IRContext::kAnalysisNone); + const Uint32 memberIndexId = builder.GetUintConstantId(target.memberIndex); + auto* access = + builder.AddAccessChain(target.vectorPtrTypeId, target.variableId, {memberIndexId}); + if (access == nullptr) return injected; + auto* value = builder.AddLoad(target.vectorTypeId, access->result_id()); + if (value == nullptr) return injected; + builder.AddStore(mirrorVariableId, value->result_id()); + injected = true; + } + } + return injected; + } + + Vector m_varyings; + Vector m_strides; + }; + // Adreno 650 (driver 512.502) faults the GPU on an implicit-LOD sample of a full-screen // colour render target: the texture unit's derivative path reads outside the image's // allocation even though the sampler clamps LOD to 0 and the mapping is 1:1. MobileGL's @@ -1128,6 +1285,41 @@ namespace MobileGL::MG_Backend::DirectVulkan { return spvtools::Optimizer::PassToken(MakeUnique(transformFlags)); } + Bool TransformSpirvForXfbCapture(const Vector& input, Vector& output, + const MG_State::GLState::ProgramObject& program) { + if (input.empty()) { + output.clear(); + return true; + } + Vector varyings; + varyings.reserve(program.GetTransformFeedbackVaryingCount()); + for (const auto& varying : program.GetTransformFeedbackVaryings()) { + varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes}); + } + Vector strides; + strides.reserve(program.GetTransformFeedbackBufferCount()); + for (SizeT i = 0; i < program.GetTransformFeedbackBufferCount(); ++i) { + strides.push_back(program.GetTransformFeedbackStride(static_cast(i))); + } + + spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3); + spvtools::OptimizerOptions options; + options.set_run_validator(false); + optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, + const char* message) { + MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : ""); + }); + optimizer.RegisterPass(spvtools::Optimizer::PassToken( + MakeUnique(Move(varyings), Move(strides)))); + + const Bool success = optimizer.Run(input.data(), input.size(), &output, options); + if (!success) { + MGLOG_E("Vulkan: xfb capture decoration pass failed; keeping the original module"); + output = input; + } + return success; + } + Bool TransformSpirvForVulkanPositionFix(const Vector& input, Vector& output, ProgramFactory::CompileOptionFlags transformFlags) { if (input.empty()) { @@ -2173,7 +2365,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Apply position fixup if needed if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) { - TransformSpirvForVulkanPositionFix(spv, moduleSpirvs[i], flags); + const Vector* fixupInput = &spv; + Vector xfbSpirv; + if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) && + program.GetTransformFeedbackVaryingCount() > 0) { + // Decorate BEFORE the position fixup so a captured gl_Position + // mirror copies the shader's own (pre-remap) value. + if (TransformSpirvForXfbCapture(spv, xfbSpirv, program)) { + fixupInput = &xfbSpirv; + } + } + TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags); } else { moduleSpirvs[i] = spv; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index 1ae8e8c8..eb0adfd0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -47,6 +47,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // level, which makes the two forms produce identical texels (the implicit lambda is // clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias). ExplicitLod0Sampling = 1 << 5, + // Decorates the last vertex-processing stage's captured varyings with + // XfbBuffer/XfbStride/Offset (VK_EXT_transform_feedback). Set only for draws + // recorded while GL transform feedback is active, so plain draws keep the + // undecorated variant. + XfbCapture = 1 << 6, }; using CompileOptionFlags = Flags; using HashType = Uint64; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 9d2396a2..22297278 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -22,6 +22,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + // Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled + // (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled). + constexpr VkBufferUsageFlags kTransformFeedbackUsage = + VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT; // The app writes into the persistent map with no explicit flush, so its memory must // be host-coherent (Adreno host-visible memory is; requiring it keeps us portable). constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags = @@ -465,7 +469,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // it from the current shadow - MappedData() is still the shadow here because the // frontend adopts (and drops) the shadow only after this returns. DeferRelease(std::move(resource->buffer)); - if (!CreateResidentStorage(*resource, size, kPersistentBackedUsage, kPersistentBackedRequiredFlags)) { + const VkBufferUsageFlags persistentUsage = + kPersistentBackedUsage | + (m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0); + if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) { resource->persistentMapped = false; resource->storageSize = 0; resource->usageFlags = 0; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index aec65f53..c1e85461 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -31,6 +31,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO; VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; Bool transientPersistentMapping = false; + // VK_EXT_transform_feedback is enabled: persistent-map storage additionally + // carries the transform feedback usage so capture targets can bind directly. + Bool transformFeedbackUsageEnabled = false; }; // The DirectVulkan storage behind one frontend buffer (pipe_resource analogue). diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index d6d7b424..5b2d2aa1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2581,6 +2581,7 @@ void main() { .transientMemoryUsage = VMA_MEMORY_USAGE_AUTO, .transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT, .transientPersistentMapping = true, + .transformFeedbackUsageEnabled = m_transformFeedbackFeatureEnabled, }); MOBILEGL_ASSERT(succeeded, "VkBufferManager initialization failed."); m_bufferManager.SetCopyCommandProvider(this); @@ -2745,6 +2746,7 @@ void main() { m_textureManager.reset(); } m_vertexInputStateFactory.reset(); + m_xfbCounterBuffer.Destroy(); m_bufferManager.Shutdown(); // Device is idle (vkDeviceWaitIdle above); query pools can be destroyed. @@ -4618,6 +4620,11 @@ void main() { const auto& vao = *MG_State::pGLContext->GetBoundVertexArray(); const auto& program = *MG_State::pGLContext->GetCurrentProgram(); ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform()); + // Captured draws take the xfb-decorated program variant. + if (m_transformFeedbackFeatureEnabled && MG_State::pGLContext->IsTransformFeedbackActive() && + program.GetTransformFeedbackVaryingCount() > 0) { + transformFlags |= ProgramFactory::CompileOptionBit::XfbCapture; + } // Sampling a colour render target through the driver's implicit-LOD path faults the GPU on // Adreno 650 (see ForceExplicitLod0SamplePass); ask for the explicit-LOD variant when doing // so cannot change a texel, i.e. when every sampler this program reads is pinned to a @@ -7292,6 +7299,100 @@ void main() { resource->layout = finalLayout; } + Bool VulkanRenderer::BeginXfbCaptureForDraw(FrameContext::FrameData& frame) { + if (!m_transformFeedbackFeatureEnabled || MG_State::pGLContext == nullptr || + !MG_State::pGLContext->IsTransformFeedbackActive()) { + return false; + } + const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram(); + if (!program || program->GetTransformFeedbackVaryingCount() == 0) { + return false; + } + const SizeT bufferCount = std::min(program->GetTransformFeedbackBufferCount(), 4); + if (bufferCount == 0) { + return false; + } + + if (!m_xfbCounterBuffer.IsValid()) { + if (!m_xfbCounterBuffer.Create({ + .allocator = m_allocator, + .size = 16, + .usage = VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_COUNTER_BUFFER_BIT_EXT | + VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memoryUsage = VMA_MEMORY_USAGE_AUTO, + })) { + MGLOG_E("BeginXfbCaptureForDraw: failed to create the counter buffer"); + return false; + } + } + + VkBuffer buffers[4] = {}; + VkDeviceSize offsets[4] = {}; + VkDeviceSize sizes[4] = {}; + for (SizeT i = 0; i < bufferCount; ++i) { + auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, + static_cast(i)); + const auto& bufferObject = point.GetBoundObject(); + if (bufferObject == nullptr) { + return false; + } + // Host-visible coherent GPU residency: the capture writes land where + // MapBuffer/GetBufferSubData read. + bufferObject->EnsureGpuResidentStorage(); + BufferSlice slice{}; + if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, bufferObject, slice)) { + MGLOG_E("BeginXfbCaptureForDraw: failed to acquire capture buffer %zu", i); + return false; + } + const Range1D range = point.GetRange(); + const VkDeviceSize rangeStart = static_cast(range.start); + const VkDeviceSize rangeSize = range.end > range.start + ? static_cast(range.end - range.start) + : VK_WHOLE_SIZE; + buffers[i] = slice.buffer; + offsets[i] = slice.offset + rangeStart; + sizes[i] = rangeSize; + } + + s_vkCmdBindTransformFeedbackBuffersEXT(frame.commandBuffer, 0, static_cast(bufferCount), buffers, + offsets, sizes); + + const Uint64 generation = MG_State::pGLContext->GetTransformFeedbackGeneration(); + const Bool resume = m_xfbCountersValid && m_xfbLastSeenGeneration == generation; + m_xfbLastSeenGeneration = generation; + + VkBuffer counterBuffers[4] = {}; + VkDeviceSize counterOffsets[4] = {}; + for (SizeT i = 0; i < bufferCount; ++i) { + counterBuffers[i] = m_xfbCounterBuffer.GetHandle(); + counterOffsets[i] = static_cast(i) * 4; + } + if (resume) { + s_vkCmdBeginTransformFeedbackEXT(frame.commandBuffer, 0, static_cast(bufferCount), + counterBuffers, counterOffsets); + } else { + s_vkCmdBeginTransformFeedbackEXT(frame.commandBuffer, 0, 0, nullptr, nullptr); + } + return true; + } + + void VulkanRenderer::EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began) { + if (!began) { + return; + } + const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram(); + const SizeT bufferCount = program ? std::min(program->GetTransformFeedbackBufferCount(), 4) : 0; + VkBuffer counterBuffers[4] = {}; + VkDeviceSize counterOffsets[4] = {}; + for (SizeT i = 0; i < bufferCount; ++i) { + counterBuffers[i] = m_xfbCounterBuffer.GetHandle(); + counterOffsets[i] = static_cast(i) * 4; + } + s_vkCmdEndTransformFeedbackEXT(frame.commandBuffer, 0, static_cast(bufferCount), counterBuffers, + counterOffsets); + m_xfbCountersValid = true; + } + void VulkanRenderer::DrawArrays(const DrawCmd& payload) { auto& frame = m_frameContext.GetCurrent(); @@ -7303,11 +7404,13 @@ void main() { VkCommandBuffer& commandBuffer = frame.commandBuffer; + const Bool xfbActive = BeginXfbCaptureForDraw(frame); vkCmdDraw(commandBuffer, payload.params.vertexCount, payload.params.instanceCount, payload.params.firstVertex, payload.params.firstInstance); + EndXfbCaptureForDraw(frame, xfbActive); } void VulkanRenderer::DrawElements(const DrawIndexedCmd& payload) { @@ -7334,12 +7437,14 @@ void main() { VkCommandBuffer& commandBuffer = frame.commandBuffer; + const Bool xfbActive = BeginXfbCaptureForDraw(frame); vkCmdDrawIndexed(commandBuffer, payload.params.indexCount, payload.params.instanceCount, payload.params.firstIndex, payload.params.vertexOffset, payload.params.firstInstance); + EndXfbCaptureForDraw(frame, xfbActive); } void VulkanRenderer::MultiDrawArrays(const MultiDrawCmd& payload) { @@ -8817,6 +8922,31 @@ void main() { } } + // VK_EXT_transform_feedback backs GL transform feedback capture. + m_transformFeedbackFeatureEnabled = false; + VkPhysicalDeviceTransformFeedbackFeaturesEXT transformFeedbackFeatures{}; + transformFeedbackFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT; + if (IsExtensionSupported(availableExtensions, VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME) && + getPhysicalDeviceFeatures2 != nullptr) { + VkPhysicalDeviceFeatures2 featureQuery{}; + featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + featureQuery.pNext = &transformFeedbackFeatures; + getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); + if (transformFeedbackFeatures.transformFeedback == VK_TRUE) { + if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME)) { + enabledDeviceExtensions.push_back(VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME); + } + transformFeedbackFeatures.geometryStreams = VK_FALSE; + transformFeedbackFeatures.pNext = const_cast(deviceCreateInfo.pNext); + deviceCreateInfo.pNext = &transformFeedbackFeatures; + m_transformFeedbackFeatureEnabled = true; + MGLOG_I("Enabled optional device extension: %s", VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME); + } + } + if (!m_transformFeedbackFeatureEnabled) { + MGLOG_W("VK_EXT_transform_feedback is unavailable; transform feedback capture will not work"); + } + deviceCreateInfo.enabledExtensionCount = static_cast(enabledDeviceExtensions.size()); deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data(); MGLOG_I("Device feature support: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " @@ -8872,6 +9002,20 @@ void main() { MGLOG_W("VK_KHR_draw_indirect_count enabled but vkCmdDrawIndexedIndirectCount entry point is missing, will continue as if VK_KHR_draw_indirect_count is not supported!"); m_drawIndirectCountExtensionEnabled = false; } + + if (m_transformFeedbackFeatureEnabled) { + s_vkCmdBindTransformFeedbackBuffersEXT = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdBindTransformFeedbackBuffersEXT")); + s_vkCmdBeginTransformFeedbackEXT = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdBeginTransformFeedbackEXT")); + s_vkCmdEndTransformFeedbackEXT = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdEndTransformFeedbackEXT")); + if (s_vkCmdBindTransformFeedbackBuffersEXT == nullptr || s_vkCmdBeginTransformFeedbackEXT == nullptr || + s_vkCmdEndTransformFeedbackEXT == nullptr) { + MGLOG_W("VK_EXT_transform_feedback entry points missing; transform feedback capture disabled"); + m_transformFeedbackFeatureEnabled = false; + } + } MGLOG_I("index type uint8 enabled: %s", m_indexTypeUint8ExtensionEnabled ? "true" : "false"); MGLOG_I("Logical device created."); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index e2ee4a42..04882636 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -455,6 +455,23 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 stride); static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr; + // VK_EXT_transform_feedback (GL transform feedback capture) + Bool m_transformFeedbackFeatureEnabled = false; + static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr; + static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr; + static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr; + // Counter buffers (one 4-byte slot per capture binding) let consecutive + // draws within one glBeginTransformFeedback append GL-style. + VkBufferObject m_xfbCounterBuffer; + // Non-zero while inside a GL Begin/End with at least one captured draw + // recorded; selects counter-buffer resume on the next captured draw. + Bool m_xfbCountersValid = false; + Uint64 m_xfbLastSeenGeneration = 0; + // Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT + // when GL transform feedback is active; binds capture buffers on demand. + Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame); + void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began); + VkCommandPool m_commandPool = VK_NULL_HANDLE; VkBufferManager m_bufferManager; diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 5bc4ee39..13edd611 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -67,8 +67,12 @@ namespace MobileGL::MG_Impl::GLImpl { } // While transform feedback is active the draw's primitive type must match - // the feedback primitive mode (GL 3.3 core 13.2.2). - if (MG_State::pGLContext->IsTransformFeedbackActive()) { + // the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader + // the constraint moves to the shader's output primitive type instead, so + // the draw mode itself is unconstrained here. + if (MG_State::pGLContext->IsTransformFeedbackActive() && + !(MG_State::pGLContext->GetTransformFeedbackProgram() && + MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) { const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode(); Bool compatible = false; switch (feedbackMode) { @@ -514,6 +518,18 @@ namespace MobileGL::MG_Impl::GLImpl { return; } MG_State::pGLContext->EndTransformFeedback(); + // Captured results must be visible to MapBuffer/GetBufferSubData after + // End; the capture targets are host-coherent GPU memory, so completing + // the GPU work is all that is required. + auto& backendGL = MG_Backend::gBackendFunctionsTable.GL; + if (backendGL.FenceSync && backendGL.ClientWaitSync) { + if (auto sync = backendGL.FenceSync()) { + backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull); + if (backendGL.DeleteSync) { + backendGL.DeleteSync(sync); + } + } + } } } // namespace MobileGL::MG_Impl::GLImpl diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 503f4529..934e4b99 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -1932,6 +1932,12 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_MAX_VERTEX_STREAMS: *params = 1; break; + case GL_TRANSFORM_FEEDBACK_ACTIVE: + *params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0; + break; + case GL_TRANSFORM_FEEDBACK_PAUSED: + *params = 0; + break; case GL_MAX_TEXTURE_IMAGE_UNITS: *params = dynamicParameters.MaxTextureImageUnits; break; diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index 3aff2d5e..a69a462a 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -231,6 +231,21 @@ namespace MobileGL::MG_State::GLState { return m_resource.Bytes(); } + Bool BufferObject::EnsureGpuResidentStorage() { + if (m_resource.IsGpuResident()) { + return true; + } + if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) { + return false; + } + void* base = g_bufferBackendOps->AcquirePersistentMap(*this); + if (base == nullptr) { + return false; + } + m_resource.AdoptPersistentMap(base); + return true; + } + void* BufferObject::AcquireMemoryRange(Range1D range, Flags access) { MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end, "AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start, diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.h b/MobileGL/MG_State/GLState/BufferState/BufferObject.h index a397721e..6c441ea6 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.h @@ -133,6 +133,11 @@ namespace MobileGL { void* AcquireMemory(Bool markMapped, Bool read, Bool write); void* AcquireMemoryRange(Range1D range, Flags access); + // Adopt backend host-visible coherent GPU storage as the source of truth + // (used for GPU-written targets like transform feedback capture, so + // MapBuffer/GetBufferSubData read real GPU results). No-op when already + // resident or when the backend declines. + Bool EnsureGpuResidentStorage(); void ReleaseMemory(); void FlushMemoryRange(SizeT offset, SizeT length); diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 283e1285..33e78c35 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -216,6 +216,7 @@ namespace MobileGL { m_transformFeedbackActive = true; m_transformFeedbackPrimitiveMode = primitiveMode; m_transformFeedbackProgram = program; + ++m_transformFeedbackGeneration; } void EndTransformFeedback() { m_transformFeedbackActive = false; @@ -226,6 +227,9 @@ namespace MobileGL { const SharedPtr& GetTransformFeedbackProgram() const { return m_transformFeedbackProgram; } + // Bumped on every BeginTransformFeedback; the backend uses it to + // distinguish "resume appending" from "fresh capture". + Uint64 GetTransformFeedbackGeneration() const { return m_transformFeedbackGeneration; } // Framebuffer void GenFramebufferNames(Uint number, Vector& framebuffers); @@ -262,6 +266,7 @@ namespace MobileGL { Bool m_transformFeedbackActive = false; GLenum m_transformFeedbackPrimitiveMode = GL_POINTS; SharedPtr m_transformFeedbackProgram; + Uint64 m_transformFeedbackGeneration = 0; TextureState m_textureState; ProgramState m_programState; RenderState m_renderState;