diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 450cb48c..65093f57 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -365,6 +365,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void BackendObject_DirectVulkan::Initialize() { +#if MOBILEGL_BUILD_DISAGGREGATED + VkBufferManager::RegisterWireResourceOps(); +#endif m_initialized = true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index eb2a2fe3..476ec41e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -282,6 +282,68 @@ namespace MobileGL::MG_Backend::DirectVulkan { return cache; } +#if MOBILEGL_BUILD_DISAGGREGATED + // The verb's handles identify server stores. Readback orders GPU-produced + // command/count bytes before CPU tier expansion; never inspect a client binding. + void DrawWireIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, + GLsizei stride, Bool indexed, Bool counted = false, GLintptr countOffset = 0) { + if (drawcount <= 0) return; + auto& buffers = pVulkanRenderer->GetWireBufferManager(); + const auto& state = MG_Pipe::MGPipeApplier(); + if (counted) { + Uint32 count = 0; + if (countOffset < 0 || !buffers.ReadWireBuffer(state.VerbIndirectParameterBuffer, + static_cast(countOffset), sizeof(count), &count)) return; + drawcount = static_cast(std::min(count, static_cast(drawcount))); + if (!drawcount) return; + } + const SizeT commandSize = indexed ? sizeof(DrawElementsIndirectCommand) : sizeof(DrawArraysIndirectCommand); + if (stride == 0) stride = static_cast(commandSize); + if (stride < static_cast(commandSize)) return; + const Uint64 byteCount = static_cast(stride) * (drawcount - 1) + commandSize; + if (byteCount > std::numeric_limits::max()) return; + Vector bytes(static_cast(byteCount)); + if (!buffers.ReadWireBuffer(state.VerbIndirectBuffer, reinterpret_cast(indirect), + byteCount, bytes.data())) return; + if (indexed) { + const SizeT indexSize = MG_Util::GetGLTypeSize(type); + if (indexSize != 1 && indexSize != 2 && indexSize != 4) return; + Vector params(static_cast(drawcount)); + MultiDrawIndexedCmd payload{}; + payload.mode = mode; + payload.indexBufferView.indexType = type; + payload.drawCount = static_cast(drawcount); + payload.pParams = params.data(); + for (GLsizei i = 0; i < drawcount; ++i) { + DrawElementsIndirectCommand command{}; + Memcpy(&command, bytes.data() + static_cast(i) * stride, sizeof(command)); + params[i] = {command.count, command.instanceCount, command.firstIndex, + command.baseVertex, command.baseInstance}; + const Uint64 end = (static_cast(command.firstIndex) + command.count) * indexSize; + if (end > std::numeric_limits::max()) return; + payload.indexBufferView.indexByteSize = std::max(payload.indexBufferView.indexByteSize, + static_cast(end)); + } + pVulkanRenderer->MultiDrawElements(payload); + } else { + Vector params(static_cast(drawcount)); + for (GLsizei i = 0; i < drawcount; ++i) { + DrawArraysIndirectCommand command{}; + Memcpy(&command, bytes.data() + static_cast(i) * stride, sizeof(command)); + params[i].vertexCount = command.count; + params[i].instanceCount = command.instanceCount; + params[i].firstVertex = command.first; + params[i].firstInstance = command.baseInstance; + } + MultiDrawCmd payload{}; + payload.mode = mode; + payload.drawCount = static_cast(drawcount); + payload.pParams = params.data(); + pVulkanRenderer->MultiDrawArrays(payload); + } + } +#endif + MG_State::GLState::ProgramObject* TryGetDirectVulkanProgram(GLuint program) { if (!MGB_CTX->ValidateProgramName(program)) { return nullptr; @@ -402,7 +464,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { #if MOBILEGL_BUILD_DISAGGREGATED - RejectWireLegacyBuffer(); + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + DrawWireIndirect(mode, type, indirect, drawcount, stride, true); + return; + } #endif MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirect called with null VulkanRenderer"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsIndirect called with null GL context"); @@ -410,7 +475,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) { #if MOBILEGL_BUILD_DISAGGREGATED - RejectWireLegacyBuffer(); + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + DrawWireIndirect(mode, 0, indirect, drawcount, stride, false); + return; + } #endif MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirect called with null VulkanRenderer"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArraysIndirect called with null GL context"); @@ -464,7 +532,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) { #if MOBILEGL_BUILD_DISAGGREGATED - RejectWireLegacyBuffer(); + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + DrawWireIndirect(mode, type, indirect, maxdrawcount, stride, true, true, drawcount); + return; + } #endif MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirectCount called with null VulkanRenderer"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsIndirectCount called with null GL context"); @@ -473,7 +544,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) { #if MOBILEGL_BUILD_DISAGGREGATED - RejectWireLegacyBuffer(); + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + DrawWireIndirect(mode, 0, indirect, maxdrawcount, stride, false, true, drawcount); + return; + } #endif MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirectCount called with null VulkanRenderer"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArraysIndirectCount called with null GL context"); @@ -548,7 +622,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { #if MOBILEGL_BUILD_DISAGGREGATED - RejectWireLegacyBuffer(); + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + DrawWireIndirect(mode, type, indirect, 1, 0, true); + return; + } #endif MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsIndirect called with null VulkanRenderer"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsIndirect called with null GL context"); @@ -610,7 +687,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void DrawArraysIndirect(GLenum mode, const void* indirect) { #if MOBILEGL_BUILD_DISAGGREGATED - RejectWireLegacyBuffer(); + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + DrawWireIndirect(mode, 0, indirect, 1, 0, false); + return; + } #endif MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysIndirect called with null VulkanRenderer"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArraysIndirect called with null GL context"); @@ -681,7 +761,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { void DispatchComputeIndirect(GLintptr indirect) { #if MOBILEGL_BUILD_DISAGGREGATED - RejectWireLegacyBuffer(); + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + Uint32 groups[3]{}; + if (indirect >= 0 && pVulkanRenderer->GetWireBufferManager().ReadWireBuffer( + MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer, + static_cast(indirect), sizeof(groups), groups)) + pVulkanRenderer->DispatchCompute(groups[0], groups[1], groups[2]); + return; + } #endif MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchComputeIndirect called with null VulkanRenderer"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DispatchComputeIndirect called with null GL context"); @@ -966,9 +1053,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // vkCmdDrawIndexed all carry natively. static void MultiDrawElementsImpl(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount, const GLint* basevertex) { -#if MOBILEGL_BUILD_DISAGGREGATED - RejectWireLegacyBuffer(); -#endif + if (drawcount <= 0) { return; } @@ -983,8 +1068,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { // MultiDrawIndexedCmd left the client-memory shape addressing a view whose byte // offset is a hardcoded 0, so UploadAndBindIndexBuffer saw a null client pointer, // declined the whole batch and painted nothing.) +#if MOBILEGL_BUILD_DISAGGREGATED + const Bool noIndexBuffer = MG_Config::Transport != MG_Config::TransportMode::Monolith + ? MG_Pipe::MGPipeHandleIsNull(MG_Pipe::MGPipeApplier().IndexBuffer.Res) + : MGB_CTX->GetBoundVertexArray()->GetIndexBufferBindingSlot().GetBoundObject() == nullptr; + if (noIndexBuffer) { +#else const auto& vao = *MGB_CTX->GetBoundVertexArray(); if (vao.GetIndexBufferBindingSlot().GetBoundObject() == nullptr) { +#endif for (GLsizei i = 0; i < drawcount; ++i) { if (count[i] <= 0) { continue; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/BufferArena.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/BufferArena.cpp index ba7b6c7f..b7f9eede 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/BufferArena.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/BufferArena.cpp @@ -7,6 +7,9 @@ // End of Source File Header #include "BufferArena.h" +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#endif namespace MobileGL::MG_Backend::DirectVulkan { Bool BufferArena::Initialize(const BufferArenaDesc& desc) { @@ -125,6 +128,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { bufferDesc.usage = m_desc.usage; bufferDesc.memoryUsage = m_desc.memoryUsage; bufferDesc.allocationFlags = m_desc.allocationFlags; +#if MOBILEGL_BUILD_DISAGGREGATED + // Split staging may also be the destination of a GPU UBO range copy. + // Persistent host writes must be coherent: Upload deliberately writes + // the mapped pointer without a per-range VMA flush. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith && m_desc.persistentlyMapped) { + bufferDesc.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + } +#endif if (!buffer.Create(bufferDesc)) { return false; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 1fc14a04..deb1c70f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -46,9 +46,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::abort(); } - [[noreturn]] static void WireBufferDescriptorDebt() { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} - Magma buffer descriptors remain P7"); - std::abort(); + static Bool ResolveWireRange(Uint64 offset, Uint64 declaredSize, VkDeviceSize bufferSize, + VkDeviceSize& start, VkDeviceSize& size) { + if (offset >= bufferSize) return false; + start = static_cast(offset); + const VkDeviceSize remaining = bufferSize - start; + size = declaredSize == MG_Pipe::kMGPipeWholeBuffer + ? remaining : std::min(declaredSize, remaining); + return size != 0; } static VkImageViewType WireViewType(MG_Pipe::MGPipeResourceTarget target) { @@ -474,6 +479,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_bufferManager = bufferManager; m_programFactory = programFactory; m_minDynamicOffsetAlignment = std::max(1, minUniformBufferOffsetAlignment); +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(m_physicalDevice, &properties); + m_wireStorageOffsetAlignment = std::max(1, properties.limits.minStorageBufferOffsetAlignment); + m_wireTexelOffsetAlignment = std::max(1, properties.limits.minTexelBufferOffsetAlignment); + m_wireMaxUniformRange = properties.limits.maxUniformBufferRange; + m_wireMaxStorageRange = properties.limits.maxStorageBufferRange; + m_wireMaxTexelElements = properties.limits.maxTexelBufferElements; + } +#endif m_frameCount = frameCount; m_maxBindings = maxBindings; m_samplerResolveMemo.assign(m_maxBindings, SamplerResolveMemo{}); @@ -1052,27 +1068,75 @@ namespace MobileGL::MG_Backend::DirectVulkan { return texture; } +#if MOBILEGL_BUILD_DISAGGREGATED + Bool UniformManager::ResolveWireTexelBufferDescriptor(const MagmaProgramSource& program, + const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, + Bool storage, VkBufferView& out) { + out = VK_NULL_HANDLE; + if (binding >= programObj.samplerUniformLocationByBinding.size() || + binding >= programObj.samplerNumericDomainByBinding.size() || frameIndex >= m_frames.size()) return false; + const Int location = programObj.samplerUniformLocationByBinding[binding]; + if (location < 0) return false; + const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); + const auto& state = MG_Pipe::MGPipeApplier(); + if (unit < 0 || static_cast(unit) >= + (storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) return false; + if (storage && binding >= programObj.storageImageFormatByBinding.size()) return false; + const VkFormat declaredFormat = storage ? programObj.storageImageFormatByBinding[binding] : VK_FORMAT_UNDEFINED; + const auto placeholder = [&] { + out = AcquireUnboundTexelBufferView(declaredFormat, + programObj.samplerNumericDomainByBinding[binding], storage); + return out != VK_NULL_HANDLE; + }; + const auto texture = storage ? state.BoundShaderImages[unit].Res : state.BoundSamplerViews[unit].Texture; + if (MG_Pipe::MGPipeHandleIsNull(texture)) return placeholder(); + if (texture.Slot >= state.TextureResources.size()) WireDescriptorFatal("texel-buffer-record"); + const auto& record = state.TextureResources[texture.Slot]; + if (!record.Live || record.Gen != texture.Gen) WireDescriptorFatal("texel-buffer-generation"); + if (record.Desc.Target != static_cast(MG_Pipe::MGPipeResourceTarget::TexBuffer)) + WireDescriptorFatal("texel-buffer-target"); + const auto buffer = record.Desc.BufferForTexBuffer; + if (MG_Pipe::MGPipeHandleIsNull(buffer)) return placeholder(); + + BufferSlice slice{}; + if (!m_bufferManager->AcquireWireSlice(BufferKind::TextureBuffer, buffer, slice) || !slice.IsValid()) return false; + const auto internalFormat = static_cast(record.Desc.InternalFormat); + VkFormat format = declaredFormat; + if (storage && format == VK_FORMAT_UNDEFINED && state.BoundShaderImages[unit].InternalFormat != 0) + format = MG_Util::ConvertTextureInternalFormatToVkEnum(MG_Util::ConvertGLEnumToTextureInternalFormat( + state.BoundShaderImages[unit].InternalFormat)); + if (format == VK_FORMAT_UNDEFINED) format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat); + const auto feature = storage ? VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_BIT : VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT; + if (format == VK_FORMAT_UNDEFINED || !BufferFormatSupportsFeature(m_physicalDevice, format, feature)) + WireDescriptorFatal("texel-buffer-native-format@P7"); + const VkDeviceSize texelSize = MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat); + VkDeviceSize start = 0, size = 0; + if (texelSize == 0 || !ResolveWireRange(record.Desc.BufOffset, record.Desc.BufSize, slice.size, start, size)) return false; + size = std::min(size / texelSize, static_cast(m_wireMaxTexelElements)) * texelSize; + if (size == 0) return false; + if (start > std::numeric_limits::max() - slice.offset || + (slice.offset + start) % m_wireTexelOffsetAlignment != 0) + WireDescriptorFatal("unaligned-texel-buffer-range@P7"); + VkBufferViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO}; + viewInfo.buffer = slice.buffer; + viewInfo.format = format; + viewInfo.offset = slice.offset + start; + viewInfo.range = size; + if (vkCreateBufferView(m_device, &viewInfo, nullptr, &out) != VK_SUCCESS || out == VK_NULL_HANDLE) return false; + m_frames[frameIndex].texelBufferViews.push_back(out); + if (storage && state.BoundShaderImages[unit].Access != kMGPipeImageAccessReadOnly) + m_bufferManager->MarkWireBufferGpuWritten(buffer, start, size); + return true; + } +#endif + Bool UniformManager::ResolveTexelBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView) { #if MOBILEGL_BUILD_DISAGGREGATED - if (program.IsWire()) { - const Int location = programObj.samplerUniformLocationByBinding[binding]; - const Int unit = location < 0 ? 0 : program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); - const auto& state = MG_Pipe::MGPipeApplier(); - if (unit < 0 || static_cast(unit) >= MG_Pipe::kMGPipeMaxTextureUnits) return false; - const auto handle = state.BoundSamplerViews[unit].Texture; - if (!MG_Pipe::MGPipeHandleIsNull(handle)) { - if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("texel-buffer-record"); - const auto& record = state.TextureResources[handle.Slot]; - if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("texel-buffer-generation"); - if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.BufferForTexBuffer)) WireBufferDescriptorDebt(); - } - outBufferView = AcquireUnboundTexelBufferView(VK_FORMAT_UNDEFINED, - programObj.samplerNumericDomainByBinding[binding], false); - return outBufferView != VK_NULL_HANDLE; - } + if (program.IsWire()) + return ResolveWireTexelBufferDescriptor(program, programObj, binding, frameIndex, false, outBufferView); #endif outBufferView = VK_NULL_HANDLE; MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveTexelBufferDescriptor: buffer manager is null"); @@ -1199,22 +1263,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView) { #if MOBILEGL_BUILD_DISAGGREGATED - if (program.IsWire()) { - const Int location = programObj.samplerUniformLocationByBinding[binding]; - const Int unit = location < 0 ? 0 : program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); - const auto& state = MG_Pipe::MGPipeApplier(); - if (unit < 0 || static_cast(unit) >= MG_Pipe::kMGPipeMaxImageUnits) return false; - const auto handle = state.BoundShaderImages[unit].Res; - if (!MG_Pipe::MGPipeHandleIsNull(handle)) { - if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("texel-buffer-record"); - const auto& record = state.TextureResources[handle.Slot]; - if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("texel-buffer-generation"); - if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.BufferForTexBuffer)) WireBufferDescriptorDebt(); - } - outBufferView = AcquireUnboundTexelBufferView(programObj.storageImageFormatByBinding[binding], - programObj.samplerNumericDomainByBinding[binding], true); - return outBufferView != VK_NULL_HANDLE; - } + if (program.IsWire()) + return ResolveWireTexelBufferDescriptor(program, programObj, binding, frameIndex, true, outBufferView); #endif outBufferView = VK_NULL_HANDLE; MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null"); @@ -1387,10 +1437,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageBufferDescriptor: GL context is null"); MOBILEGL_ASSERT(binding < programObj.storageBlockIndexByBinding.size(), "ResolveStorageBufferDescriptor: binding %u out of range", binding); +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire() && (binding >= programObj.storageBlockIndexByBinding.size() || + binding >= programObj.storageBlockNameByBinding.size())) + WireDescriptorFatal("storage-buffer-reflection-binding"); +#endif const Int blockIndex = programObj.storageBlockIndexByBinding[binding]; MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u", binding); +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire() && blockIndex < 0) WireDescriptorFatal("storage-buffer-reflection-block"); +#endif // An atomic counter is not an SSBO the application ever declared: glslang lowers every // atomic_uint onto a synthesized gl_AtomicCounterBlock_ storage block, where N is the // GL ATOMIC-COUNTER binding. That block arrives here auto-mapped to an arbitrary @@ -1429,13 +1487,32 @@ namespace MobileGL::MG_Backend::DirectVulkan { #if MOBILEGL_BUILD_DISAGGREGATED if (program.IsWire()) { const auto& state = MG_Pipe::MGPipeApplier(); - const Uint32 cls = isAtomicCounterBlock ? 2u : 1u; - if (frontendBinding < state.ShaderBufferCount[cls] && - !MG_Pipe::MGPipeHandleIsNull(state.BoundShaderBuffers[cls][frontendBinding].Res)) - WireBufferDescriptorDebt(); - const BufferSlice placeholder = m_bufferManager->AcquireUnboundStorageDescriptor(); - if (!placeholder.IsValid()) return false; - outBufferInfo = {placeholder.buffer, placeholder.offset, placeholder.size}; + const Uint32 cls = isAtomicCounterBlock ? MG_Pipe::kMGPipeShaderBufferClassAtomicCounter + : MG_Pipe::kMGPipeShaderBufferClassShaderStorage; + if (frontendBinding >= MG_Pipe::kMGPipeMaxBufferBindingPoints) + WireDescriptorFatal("storage-buffer-binding-point"); + // The applier retains entries outside the last partial update window. Count + // describes that update, not the upper bound of the complete binding table. + const auto& range = state.BoundShaderBuffers[cls][frontendBinding]; + if (MG_Pipe::MGPipeHandleIsNull(range.Res)) { + const BufferSlice placeholder = m_bufferManager->AcquireUnboundStorageDescriptor(); + if (!placeholder.IsValid()) return false; + outBufferInfo = {placeholder.buffer, placeholder.offset, placeholder.size}; + return true; + } + BufferSlice slice{}; + if (!m_bufferManager->AcquireWireSlice(BufferKind::ShaderStorage, range.Res, slice) || !slice.IsValid()) return false; + VkDeviceSize start = 0, size = 0; + if (!ResolveWireRange(range.Offset, range.Size, slice.size, start, size)) return false; + if (start > std::numeric_limits::max() - slice.offset || + (slice.offset + start) % m_wireStorageOffsetAlignment != 0) + WireDescriptorFatal(isAtomicCounterBlock ? "unaligned-atomic-buffer-range@P7" + : "unaligned-storage-buffer-range@P7"); + if (size > m_wireMaxStorageRange) WireDescriptorFatal("storage-buffer-native-range@P7"); + outBufferInfo = {slice.buffer, slice.offset + start, size}; + // Shader writes stay in the canonical server VkBuffer. The event marks the + // client's shadow stale; neither this path nor later acquires seed it back. + m_bufferManager->MarkWireBufferGpuWritten(range.Res, start, size); return true; } #endif @@ -2234,6 +2311,59 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } +#if MOBILEGL_BUILD_DISAGGREGATED + Bool UniformManager::ResolveWireUniformBufferPayload(const MagmaProgramSource& program, + Uint32 blockIndex, Uint32 bindingPoint, UboBindResult& out) const { + if (bindingPoint >= MG_Pipe::kMGPipeMaxBufferBindingPoints) + WireDescriptorFatal("uniform-buffer-binding-point"); + const VkDeviceSize blockSize = program.GetUBOSizeAt(blockIndex); + if (blockSize == 0) return false; + if (blockSize > m_wireMaxUniformRange) WireDescriptorFatal("uniform-block-native-range@P7"); + const auto& range = MG_Pipe::MGPipeApplier().BoundShaderBuffers[ + MG_Pipe::kMGPipeShaderBufferClassUniform][bindingPoint]; + BufferSlice source{}; + VkDeviceSize start = 0, available = 0; + const Bool bound = !MG_Pipe::MGPipeHandleIsNull(range.Res); + if (bound) { + if (!m_bufferManager->AcquireWireSlice(BufferKind::Uniform, range.Res, source) || !source.IsValid()) return false; + if (!ResolveWireRange(range.Offset, range.Size, source.size, start, available)) return false; + if (start > std::numeric_limits::max() - source.offset) + WireDescriptorFatal("uniform-buffer-offset-overflow"); + const VkDeviceSize absoluteOffset = source.offset + start; + if (available >= blockSize && absoluteOffset % m_minDynamicOffsetAlignment == 0 && + absoluteOffset <= std::numeric_limits::max()) { + out.directBindable = true; + out.buffer = source.buffer; + out.range = blockSize; + out.dynamicOffset = absoluteOffset; + return true; + } + } + + // Preserve the existing short-range compatibility policy without sampling the + // client's shadow: zero the complete reflected block, then GPU-copy only bytes + // inside the GL binding range. A readback here could retire the command buffer + // whose value BindProgramUniformBuffers is currently holding. + const VkDeviceSize copied = std::min(available, blockSize); + if (bound && ((start & 3u) != 0 || ((source.offset + start) & 3u) != 0 || (copied & 3u) != 0)) + WireDescriptorFatal("uniform-buffer-byte-tail@P7"); + static thread_local Vector zero; + zero.assign(static_cast(blockSize), 0); + BufferSlice padded{}; + if (!m_bufferManager->UploadTransient(BufferKind::Uniform, m_wireFrameIndex, zero.data(), blockSize, + std::max(4, m_minDynamicOffsetAlignment), padded) || !padded.IsValid()) return false; + if (bound && copied != 0 && + !m_bufferManager->CopyWireBufferRangeToSlice(range.Res, start, copied, padded)) return false; + if (padded.offset > std::numeric_limits::max()) + WireDescriptorFatal("uniform-buffer-dynamic-offset@P7"); + out.directBindable = true; + out.buffer = padded.buffer; + out.range = blockSize; + out.dynamicOffset = padded.offset; + return true; + } +#endif + Bool UniformManager::ResolveUniformBufferPayload(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, UboBindResult& out) const { @@ -2260,13 +2390,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { return outData != nullptr && outSize > 0; } -#if MOBILEGL_BUILD_DISAGGREGATED - // Global uniforms above are record bytes; named UBOs retain P7's buffer boundary. - if (program.IsWire()) WireBufferDescriptorDebt(); -#endif - MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(), "ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding); +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire() && binding >= programObj.uniformBlockIndexByBinding.size()) + WireDescriptorFatal("uniform-buffer-reflection-binding"); +#endif Int blockIndex = programObj.uniformBlockIndexByBinding[binding]; if (arrayElement > 0) { const auto arrayIt = programObj.arrayedUniformBlockIndicesByBinding.find(binding); @@ -2287,8 +2416,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(static_cast(blockIndex) < activeUniformBlockCount, "ResolveUniformBufferPayload: uniform block index %d out of range (count=%u)", blockIndex, activeUniformBlockCount); +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire() && (blockIndex < 0 || static_cast(blockIndex) >= activeUniformBlockCount)) + WireDescriptorFatal("uniform-buffer-reflection-block"); +#endif const Uint32 frontendBinding = program.GetUniformBlockBinding(static_cast(blockIndex)); +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) + return ResolveWireUniformBufferPayload(program, static_cast(blockIndex), frontendBinding, out); +#endif const Uint32 uniformBindingPointCount = static_cast(MGB_CTX->GetBufferBindingPointCount(BufferTarget::Uniform)); MOBILEGL_ASSERT(frontendBinding < uniformBindingPointCount, @@ -2535,6 +2672,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32& outDynamicOffset) { UboBindResult ubo{}; const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, arrayElement, ubo); +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire() && !hasPayload) return false; +#endif MOBILEGL_ASSERT(hasPayload && (ubo.directBindable || (ubo.payload != nullptr && ubo.payloadSize > 0)), "UniformDescriptorBinder::ResolveDynamicUboDescriptor failed: missing UBO payload on binding %u element %u", binding, arrayElement); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 6ba7631d..32cf0132 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -236,7 +236,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool ResolveWireImageDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, Bool storage, VkDescriptorImageInfo& out) const; + Bool ResolveWireTexelBufferDescriptor(const MagmaProgramSource& program, + const ProgramFactory::VkProgramObject& programObj, Uint32 binding, + Uint32 frameIndex, Bool storage, VkBufferView& out); Uint32 m_wireFrameIndex = 0; + VkDeviceSize m_wireStorageOffsetAlignment = 1; + VkDeviceSize m_wireTexelOffsetAlignment = 1; + VkDeviceSize m_wireMaxUniformRange = ~VkDeviceSize{0}; + VkDeviceSize m_wireMaxStorageRange = ~VkDeviceSize{0}; + Uint32 m_wireMaxTexelElements = ~Uint32{0}; #endif Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride, VkDescriptorImageInfo& outImageInfo) const; @@ -273,6 +281,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { const void* payload = nullptr; // fallback UploadTransient path VkDeviceSize payloadSize = 0; }; +#if MOBILEGL_BUILD_DISAGGREGATED + Bool ResolveWireUniformBufferPayload(const MagmaProgramSource& program, Uint32 blockIndex, + Uint32 bindingPoint, UboBindResult& out) const; +#endif Bool ResolveUniformBufferPayload(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, UboBindResult& out) const; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp index 7e96cf96..9a5d37a3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp @@ -435,6 +435,78 @@ namespace MobileGL::MG_Backend::DirectVulkan { return entry; } +#if MOBILEGL_BUILD_DISAGGREGATED + Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements, + const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const { + out = BackendVertexInputState{}; + for (Uint32 location = 0; location < elements.AttributeCount; ++location) { + const auto& attr = elements.Attributes[location]; + if (!attr.Enabled || !(activeMask & (1u << location))) continue; + // set_vertex_buffers is flattened PER ATTRIBUTE (VertexInputEmit.h), not + // indexed by the original ARB binding point in attr.BindingIndex. + if (location < state.VertexBufferStart || + location - state.VertexBufferStart >= state.VertexBufferCount) return false; + const auto& buffer = state.VertexBuffers[location]; + const auto type = static_cast(attr.Type); + if (attr.Stride < 0 || attr.Size < 1 || attr.Size > 4) return false; + VkFormat format = ToVkVertexFormat(type, attr.Size, attr.Normalized, attr.IsInteger, + attr.IsBgra, attr.IsLong); + auto conversion = VertexStreamConversion::None; + if (format == VK_FORMAT_UNDEFINED && type == DataType::Float64) { + const auto* backend = MG_Remote::Server::ServerLoopInstance().Backend(); + if (backend && backend->GetDynamicParameters().SupportsFloat64VertexAttributes) return false; + format = ToFloat32VertexFormat(attr.Size); + conversion = VertexStreamConversion::Float64ToFloat32; + } + if (format == VK_FORMAT_UNDEFINED) return false; + if (!SupportsVertexBufferFormat(format)) { + if (!IsScaledIntegerVertexFormat(format)) return false; + format = ToFloat32VertexFormat(attr.Size); + conversion = VertexStreamConversion::ScaledIntegerToFloat32; + if (!SupportsVertexBufferFormat(format)) return false; + } + const SizeT elementSize = GetAttributeByteSize(type, attr.Size, attr.IsBgra); + if (!elementSize) return false; + const Uint64 offset = attr.Offset + buffer.Offset; + if (offset < attr.Offset) return false; + const SizeT alignment = (type == DataType::Int2101010Rev || type == DataType::Uint2101010Rev) + ? elementSize : GetComponentSize(type); + if (conversion == VertexStreamConversion::None && alignment > 1 && + (offset % alignment || static_cast(attr.Stride) % alignment)) + conversion = VertexStreamConversion::Repack; + Uint32 stride = static_cast(attr.Stride); + if (stride && conversion != VertexStreamConversion::None) + stride = conversion == VertexStreamConversion::Repack ? static_cast(elementSize) + : static_cast(attr.Size) * sizeof(Float); + const Uint32 binding = static_cast(out.bindings.size()); + out.bindings.push_back({binding, stride, + buffer.Divisor ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX}); + out.attributes.push_back({location, binding, format, 0}); + out.bindingAttributeLocations.push_back(location); + out.bindingBaseOffsets.push_back(static_cast(offset)); + out.bindingConversions.push_back(conversion); + if (buffer.Divisor > 1) out.bindingDivisors.push_back({binding, buffer.Divisor}); + out.attributeLocationMask |= 1u << location; + } + // Native handles/offsets do not decide the pipeline layout. Include only the + // resolved binding/attribute/divisor values, including a legal zero stride. + Uint64 hash = XXH64(out.bindings.data(), out.bindings.size() * sizeof(out.bindings[0]), 0); + hash = XXH64(out.attributes.data(), out.attributes.size() * sizeof(out.attributes[0]), hash); + out.layoutHash = XXH64(out.bindingDivisors.data(), + out.bindingDivisors.size() * sizeof(out.bindingDivisors[0]), hash); + out.state.vertexBindingDescriptionCount = static_cast(out.bindings.size()); + out.state.pVertexBindingDescriptions = out.bindings.data(); + out.state.vertexAttributeDescriptionCount = static_cast(out.attributes.size()); + out.state.pVertexAttributeDescriptions = out.attributes.data(); + if (!out.bindingDivisors.empty()) { + out.divisorState.vertexBindingDivisorCount = static_cast(out.bindingDivisors.size()); + out.divisorState.pVertexBindingDivisors = out.bindingDivisors.data(); + out.state.pNext = &out.divisorState; + } + return true; + } +#endif + void VertexInputStateFactory::OnFrameBoundary() { ++m_frameBoundaryCounter; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h index b93ad771..f974c02c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h @@ -10,6 +10,9 @@ // MG_Pipe::MGPipeHandle for the P2 D12.5 memo table below. A header of constexpr constants, // so the pull build gains nothing from it. #include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#endif #include "Config.h" #include "MagmaPipeArms.h" @@ -125,6 +128,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { // normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for // an unknown/unsupported type. static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra); +#if MOBILEGL_BUILD_DISAGGREGATED + // A wire VAO is a layout record plus a separate per-attribute buffer window. + // No frontend VAO identity, address or memo participates in this layout. + Bool BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements, + const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, + BackendVertexInputState& out) const; +#endif private: static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index facba705..59c4917d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -11,6 +11,13 @@ #include "VulkanRenderer.h" #include "MG_Util/Metrics/PipeStats.h" +#if MOBILEGL_BUILD_DISAGGREGATED +#include "MG_Pipe/MGPipeCallbacks.h" +#include "MG_Pipe/PipeApply.h" +#include "MG_Remote/Server/StagedShadow.h" +#include +#include +#endif namespace MobileGL::MG_Backend::DirectVulkan { namespace { @@ -119,8 +126,272 @@ namespace MobileGL::MG_Backend::DirectVulkan { .AcquirePersistentMap = Ops_AcquirePersistentMap, .ReadbackFromGpu = Ops_ReadbackFromGpu, }; + +#if MOBILEGL_BUILD_DISAGGREGATED + VkBufferManager& WireManager() { + if (g_activeBufferManager == nullptr) { + MGLOG_F("Magma: Fatal{ResourceUnavailable, \"wire-buffer-manager\"}"); + std::abort(); + } + return *g_activeBufferManager; + } + + const MG_Pipe::MGPipeResourceOps g_vulkanWireResourceOps = { + .Create = [](auto res, const auto& desc) { WireManager().CreateWireBuffer(res, desc); }, + .Respecify = [](auto res, const auto& desc, const void* bytes) { + WireManager().RespecifyWireBuffer(res, desc, bytes); + }, + .SubData = [](auto res, const auto& record, const void* bytes) { + WireManager().WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record), + MG_Pipe::MGPipeSubDataBufferSize(record), bytes); + }, + .SubDataResident = [](auto res, const auto& record, const void* bytes) { + WireManager().WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record), + MG_Pipe::MGPipeSubDataBufferSize(record), bytes); + }, + .FlushRange = [](auto res, const auto& record, const void* bytes) { + WireManager().FlushWireBuffer(res, record.Offset, record.Size, bytes); + }, + .Readback = [](auto res, const auto& record) { + WireManager().ReadbackWireBuffer(res, record.Offset, record.Size); + }, + .Destroy = [](auto res) { WireManager().DestroyWireBuffer(res); }, + // Split mapping remains T2: the client owns its map and pushes exact + // modified ranges. A server pointer is never donated across the wire. + .MapPersistent = [](MG_Pipe::MGPipeHandle, Uint64, const void*) -> void* { return nullptr; }, + .UnmapPersistent = [](MG_Pipe::MGPipeHandle) {}, + }; +#endif } // namespace +#if MOBILEGL_BUILD_DISAGGREGATED + void VkBufferManager::RegisterWireResourceOps() { + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + MG_Pipe::MGPipeSetResourceOps(&g_vulkanWireResourceOps); + } + } + + VkBufferManager::WireBufferResource* VkBufferManager::FindWireBuffer(MG_Pipe::MGPipeHandle res) { + const auto found = m_wireBuffers.find(WireBufferKey(res)); + return found == m_wireBuffers.end() ? nullptr : &found->second; + } + + void VkBufferManager::CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc) { + if (FindWireBuffer(res) != nullptr) { + MGLOG_F("Magma: Fatal{ProtocolCorruption, \"duplicate-buffer-create\"} {slot=%u, gen=%u}", + res.Slot, res.Gen); + std::abort(); + } + m_wireBuffers.try_emplace(WireBufferKey(res)); + RespecifyWireBuffer(res, desc, nullptr); + } + + void VkBufferManager::RespecifyWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc, + const void* initialBytes) { + auto* resource = FindWireBuffer(res); + if (!resource) { + MGLOG_F("Magma: Fatal{ProtocolCorruption, \"unknown-buffer-respecify\"} {slot=%u, gen=%u}", + res.Slot, res.Gen); + std::abort(); + } + DeferRelease(std::move(resource->buffer)); + resource->size = desc.Width; + resource->lastUseSerial = 0; + resource->gpuWritesPending = false; + resource->stagedCoverage.clear(); + ++m_sliceEpochCounter; + if (resource->size == 0) return; + + VkBufferUsageFlags usage = kPersistentBackedUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + if (m_initInfo.transformFeedbackUsageEnabled) usage |= VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT; + if (!resource->buffer.Create({ + .allocator = m_initInfo.allocator, + .size = resource->size, + .usage = usage, + .memoryUsage = VMA_MEMORY_USAGE_AUTO, + .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT, + .requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + }) || resource->buffer.Map() == nullptr) { + MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-storage\"} {slot=%u, gen=%u, size=%llu}", + res.Slot, res.Gen, static_cast(resource->size)); + std::abort(); + } + // Under transport, defined bytes follow as resource_subdata records. An + // undefined store has no shadow to upload and no implicit zero snapshot. + if (initialBytes != nullptr && desc.HasDefinedContent) { + WriteWireBuffer(res, 0, resource->size, initialBytes); + } + } + + Bool VkBufferManager::WaitForWireBufferHostAccess(WireBufferResource& resource) { + if (resource.lastUseSerial <= GetCompletedSerial() && !resource.gpuWritesPending) return true; + if (!m_copyProvider || !pVulkanRenderer) return false; + const VkCommandBuffer commands = m_copyProvider->AcquireBufferCopyCommandBuffer(); + if (commands == VK_NULL_HANDLE) return false; + VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT | VK_ACCESS_HOST_WRITE_BIT; + vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_HOST_BIT, + 0, 1, &barrier, 0, nullptr, 0, nullptr); + if (!pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) { + return false; + } + resource.lastUseSerial = 0; + resource.gpuWritesPending = false; + return true; + } + + void VkBufferManager::WriteWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, + const void* bytes) { + auto* resource = FindWireBuffer(res); + if (!resource || offset > resource->size || size > resource->size - offset || (size && !bytes)) { + MGLOG_F("Magma: Fatal{ProtocolCorruption, \"buffer-write-range\"} {slot=%u, gen=%u}", + res.Slot, res.Gen); + std::abort(); + } + if (size == 0) return; + Bool uploaded = false; + if (resource->lastUseSerial > GetCompletedSerial()) { + // Vulkan buffer copies require four-byte aligned ranges. An odd GL + // byte update waits, then writes only the exact range; rounding it + // from a CPU shadow could overwrite neighbouring GPU-written bytes. + if (((offset | size) & 3u) == 0) { + uploaded = StagedWireRangeCopy(*resource, bytes, static_cast(offset), + static_cast(size)); + } + if (!uploaded && !WaitForWireBufferHostAccess(*resource)) { + MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-write-sync\"}"); + std::abort(); + } + } + if (!uploaded) { + uploaded = resource->buffer.Upload(bytes, size, offset); + if (uploaded && MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, size); + } + } + if (!uploaded) { + MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-upload\"}"); + std::abort(); + } + MG_Remote::Server::StagedShadowStore::CoverageAdd(resource->stagedCoverage, + static_cast(offset), + static_cast(offset + size)); + ++m_sliceEpochCounter; + } + + void VkBufferManager::FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, + const void* bytes) { + if (bytes != nullptr) { + WriteWireBuffer(res, offset, size, bytes); + return; + } + const auto* resource = FindWireBuffer(res); + if (!resource || offset > resource->size || size > resource->size - offset || + !MG_Remote::Server::StagedShadowStore::CoverageHas(resource->stagedCoverage, + static_cast(offset), static_cast(offset + size))) { + MGLOG_F("Magma: Fatal{StageSnapshotTooNarrow, \"buffer-flush\"} {slot=%u, gen=%u}", + res.Slot, res.Gen); + std::abort(); + } + // SubData has already copied the owned record bytes into GPU storage, + // ordered after earlier uses. There is no stale shadow to replay here. + } + + Bool VkBufferManager::AcquireWireSlice(BufferKind kind, MG_Pipe::MGPipeHandle res, BufferSlice& outSlice) { + (void)kind; // The store carries every buffer usage, so changing roles never orphans it. + outSlice = {}; + auto* resource = FindWireBuffer(res); + if (!resource || !resource->buffer.IsValid() || resource->size == 0) return false; + resource->lastUseSerial = m_frameSerial; + outSlice = resource->buffer.GetSlice(0, resource->size); + outSlice.mapped = nullptr; + return true; + } + + Bool VkBufferManager::ReadWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, void* dst) { + auto* resource = FindWireBuffer(res); + if (!resource || offset > resource->size || size > resource->size - offset || (size && !dst)) return false; + if (size == 0) return true; + // Concurrent GPU reads do not prevent a CPU read. Only staged copies or + // shader writes need a host visibility barrier and a submission wait; + // ordinary index/vertex inspection must not stall once per draw. + if (resource->gpuWritesPending && !WaitForWireBufferHostAccess(*resource)) return false; + if (!resource->buffer.Invalidate(size, offset)) return false; + Memcpy(dst, static_cast(resource->buffer.GetMappedData()) + offset, static_cast(size)); + return true; + } + + Bool VkBufferManager::CopyWireBufferRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, + const BufferSlice& dst) { + auto* resource = FindWireBuffer(res); + if (!resource || offset > resource->size || size > resource->size - offset || size > dst.size) return false; + if (size == 0) return true; + if (!m_copyProvider || dst.buffer == VK_NULL_HANDLE || ((offset | size | dst.offset) & 3u) != 0) return false; + const VkCommandBuffer commands = m_copyProvider->AcquireBufferCopyCommandBuffer(); + if (commands == VK_NULL_HANDLE) return false; + VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + before.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT; + before.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr); + const VkBufferCopy copy{offset, dst.offset, size}; + vkCmdCopyBuffer(commands, resource->buffer.GetHandle(), dst.buffer, 1, ©); + VkMemoryBarrier after{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + after.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + after.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + 0, 1, &after, 0, nullptr, 0, nullptr); + resource->lastUseSerial = m_frameSerial; + return true; + } + + void VkBufferManager::ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size) { + Vector bytes(static_cast(size)); + if (!ReadWireBuffer(res, offset, size, bytes.data())) { + MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-readback\"} {slot=%u, gen=%u}", res.Slot, res.Gen); + std::abort(); + } + if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) { + MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-writeback-callback\"}"); + std::abort(); + } + // The reverse producer synchronously copies into SEG_EVENT before + // this owned vector is released; no mapped Vulkan pointer crosses roles. + MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(res, offset, + {reinterpret_cast(bytes.data()), size, MG_Pipe::kMGHostSpanSegNone, 0}); + ++m_sliceEpochCounter; + } + + void VkBufferManager::MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size) { + auto* resource = FindWireBuffer(res); + if (!resource || offset > resource->size) return; + if (size == MG_Pipe::kMGPipeWholeBuffer) size = resource->size - offset; + if (size == 0 || size > resource->size - offset) return; + resource->lastUseSerial = m_frameSerial; + resource->stagedCoverage.clear(); + resource->gpuWritesPending = true; + ++m_sliceEpochCounter; + // The existing client dirty-state protocol accepts one whole-store + // notification. The store remains on the GPU and exact later SubData + // only overwrites its own range, preserving every other GPU-written byte. + const MG_Pipe::MGPRange range{0, MG_Pipe::kMGPipeWholeBuffer}; + if (MG_Pipe::gMGPipeCallbacks.OnGpuWritten == nullptr) { + MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-gpu-written-callback\"}"); + std::abort(); + } + MG_Pipe::gMGPipeCallbacks.OnGpuWritten(res, 1, &range); + } + + void VkBufferManager::DestroyWireBuffer(MG_Pipe::MGPipeHandle res) { + const auto found = m_wireBuffers.find(WireBufferKey(res)); + if (found == m_wireBuffers.end()) return; + DeferRelease(std::move(found->second.buffer)); + m_wireBuffers.erase(found); + ++m_sliceEpochCounter; + } +#endif + Bool VkBufferManager::Initialize(const VkBufferManagerInitInfo& initInfo) { Shutdown(); @@ -138,12 +409,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { } g_activeBufferManager = this; MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps); +#if MOBILEGL_BUILD_DISAGGREGATED + RegisterWireResourceOps(); +#endif return true; } void VkBufferManager::Shutdown() { if (g_activeBufferManager == this) { g_activeBufferManager = nullptr; +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Pipe::MGPipeGetResourceOps() == &g_vulkanWireResourceOps) { + MG_Pipe::MGPipeSetResourceOps(nullptr); + } +#endif if (MG_State::GLState::GetBufferBackendOps() == &g_vulkanBufferBackendOps) { MG_State::GLState::SetBufferBackendOps(nullptr); } @@ -153,6 +432,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_unboundTexelBuffer.Destroy(); DestroyAllDeferredReleases(); ReleaseAllLiveResources(); +#if MOBILEGL_BUILD_DISAGGREGATED + m_wireBuffers.clear(); +#endif m_copyProvider = nullptr; m_initInfo = {}; m_currentFrameIndex = 0; @@ -279,6 +561,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { .frameCount = m_initInfo.frameCount, .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | +#if MOBILEGL_BUILD_DISAGGREGATED + (MG_Config::Transport != MG_Config::TransportMode::Monolith ? VK_BUFFER_USAGE_TRANSFER_DST_BIT : 0) | +#endif VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .memoryUsage = m_initInfo.transientMemoryUsage, .allocationFlags = m_initInfo.transientAllocationFlags, @@ -431,6 +716,55 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } +#if MOBILEGL_BUILD_DISAGGREGATED + Bool VkBufferManager::StagedWireRangeCopy(WireBufferResource& resource, const void* data, + SizeT offset, SizeT size) { + if (!m_copyProvider) { + return false; + } + BufferSlice staging{}; + if (!m_transientUploadArena.Upload(m_currentFrameIndex, data, + static_cast(size), 16, staging)) { + return false; + } + if (MG_Util::PipeStats::Enabled()) { + // The staging fill is the host copy; the vkCmdCopyBuffer below is the device + // half of the same bytes and is not counted twice. + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast(size)); + } + VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer(); + if (commandBuffer == VK_NULL_HANDLE) { + return false; + } + + // Order the copy after every prior read/write of this buffer, both from + // in-flight frames (submission order) and from commands already recorded + // in this frame's command buffer. + VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, + &beforeBarrier, 0, nullptr, 0, nullptr); + + VkBufferCopy region{}; + region.srcOffset = staging.offset; + region.dstOffset = static_cast(offset); + region.size = static_cast(size); + vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, ®ion); + + VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, + &afterBarrier, 0, nullptr, 0, nullptr); + + resource.lastUseSerial = m_frameSerial; + resource.gpuWritesPending = true; + return true; + } + +#endif + void VkBufferManager::OnRespecify(MG_State::GLState::BufferObject& bufferObject) { auto* resource = ResourceOf(bufferObject); if (!resource) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 02275f53..6894afa6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -13,6 +13,10 @@ #include "../VkIncludes.h" #include #include +#if MOBILEGL_BUILD_DISAGGREGATED +#include "MG_Pipe/MGPipeTypes.h" +#include +#endif namespace MobileGL::MG_Backend::DirectVulkan { enum class BufferKind : Uint8 { @@ -99,6 +103,28 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool Initialize(const VkBufferManagerInitInfo& initInfo); void Shutdown(); +#if MOBILEGL_BUILD_DISAGGREGATED + // Registered before caps publication; the initialized renderer owns the storage. + static void RegisterWireResourceOps(); + // Transport resources are owned by their complete wire handle, never by a + // frontend BufferObject. Acquires expose the full GPU store, without a CPU + // pointer: CPU consumers must use ReadWireBuffer for ordered, current bytes. + Bool AcquireWireSlice(BufferKind kind, MG_Pipe::MGPipeHandle res, BufferSlice& outSlice); + Bool ReadWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, void* dst); + Bool CopyWireBufferRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, + const BufferSlice& dst); + void MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size); + + // Resource-op entry points. All run on the server apply owner. + void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc); + void RespecifyWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc, + const void* initialBytes); + void WriteWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes); + void FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes); + void ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size); + void DestroyWireBuffer(MG_Pipe::MGPipeHandle res); +#endif + // Recreate all per-frame transient arenas Bool RecreateTransientArenas(Uint32 frameCount); void BeginFrame(Uint32 frameIndex); @@ -180,6 +206,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool IsResourceBusy(const VkBufferResource& resource) const; private: +#if MOBILEGL_BUILD_DISAGGREGATED + struct WireBufferResource { + VkBufferObject buffer; + Uint64 size = 0; + Uint64 lastUseSerial = 0; + Bool gpuWritesPending = false; + // Only ranges actually submitted by resource_subdata are covered. No + // shadow is retained: flush cannot replay stale bytes over GPU writes. + Vector stagedCoverage; + }; + static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) { + return (static_cast(res.Gen) << 32) | res.Slot; + } + WireBufferResource* FindWireBuffer(MG_Pipe::MGPipeHandle res); + Bool WaitForWireBufferHostAccess(WireBufferResource& resource); +#endif Bool InitializeTransientArenas(); static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind); VkBufferResource* GetOrCreateResource(const SharedPtr& bufferObject); @@ -192,6 +234,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // in-flight and already-recorded GPU work. Bool StagedRangeCopy(VkBufferResource& resource, const void* data, SizeT offset, SizeT size); +#if MOBILEGL_BUILD_DISAGGREGATED + Bool StagedWireRangeCopy(WireBufferResource& resource, const void* data, SizeT offset, SizeT size); +#endif void DeferRelease(VkBufferObject&& buffer); void CollectDeferredReleases(Uint32 frameIndex); void DestroyAllDeferredReleases(); @@ -211,6 +256,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector> m_deferredBufferReleases; Vector>> m_deferredResourceReleases; Vector> m_liveResources; +#if MOBILEGL_BUILD_DISAGGREGATED + std::unordered_map m_wireBuffers; +#endif // Size m_liveResources had just after the last sweep; the next sweep waits for it to double. SizeT m_liveResourcesLastPruned = 0; Uint32 m_currentFrameIndex = 0; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 3900687f..0ac544c2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -141,6 +141,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // IBufferCopyCommandProvider: recording command buffer, outside any // render pass, for immediate staged buffer copies. VkCommandBuffer AcquireBufferCopyCommandBuffer() override; +#if MOBILEGL_BUILD_DISAGGREGATED + VkBufferManager& GetWireBufferManager() { return m_bufferManager; } +#endif // FrameContext::IRecordingObserver: prepares the frame's timer-query // pool (harvest + reset) right after the frame command buffer begins diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index b05e9252..fa8be3f7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -16,6 +16,42 @@ std::abort(); } + static Bool ConvertWireVertexStream(const MGPVertexAttribWire& attr, + VertexInputStateFactory::VertexStreamConversion conversion, const Uint8* source, + SizeT stride, SizeT count, Vector& output) { + using Conversion = VertexInputStateFactory::VertexStreamConversion; + const auto type = static_cast(attr.Type); + const SizeT elementBytes = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra); + if (conversion == Conversion::Repack) { + output.resize(count * elementBytes); + for (SizeT i = 0; i < count; ++i) + Memcpy(output.data() + i * elementBytes, source + i * stride, elementBytes); + return true; + } + output.resize(count * attr.Size * sizeof(Float)); + const auto convert = [&]() { + for (SizeT i = 0; i < count; ++i) { + for (SizeT component = 0; component < attr.Size; ++component) { + T value{}; + Memcpy(&value, source + i * stride + component * sizeof(T), sizeof(T)); + const Float converted = static_cast(value); + Memcpy(output.data() + (i * attr.Size + component) * sizeof(Float), &converted, sizeof(Float)); + } + } + return true; + }; + switch (type) { + case DataType::Float64: return convert.template operator()(); + case DataType::Int8: return convert.template operator()(); + case DataType::Uint8: return convert.template operator()(); + case DataType::Int16: return convert.template operator()(); + case DataType::Uint16: return convert.template operator()(); + case DataType::Int32: return convert.template operator()(); + case DataType::Uint32: return convert.template operator()(); + default: return false; + } + } + void VulkanRenderer::DestroyWireDrawPass() { // FlushPendingCommands only submits. These objects may still be referenced by // that submission, so retire this temporary pass after the queue completes. @@ -30,17 +66,17 @@ auto& state = MG_Pipe::MGPipeApplier(); const auto program = WireProgramSource(state.BoundShaderCso); if (!program.GetLinkStatus() || !program.GetSpirvStatus()) return false; - if (((aspects & DrawSetupAspect::IndexBuffer) && (!indices || !indices->forceClientMemory)) || - aspects & DrawSetupAspect::IndirectDrawBuffer || - MGB_CTX->IsTransformFeedbackActive()) WireBufferLegacyFatal(); + if (aspects & DrawSetupAspect::IndirectDrawBuffer || MGB_CTX->IsTransformFeedbackActive()) + WireBufferLegacyFatal(); const auto* fbo = state.DrawFramebuffer(); if (!fbo) MagmaWireFatal("draw-framebuffer-record"); if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("draw-flush"); DestroyWireDrawPass(); - ProgramFactory::CompileOptionFlags flags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform()); + ProgramFactory::CompileOptionFlags flags = + ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw(fbo->IsDefault)); const auto* programObject = &m_programFactory->GetOrCreateProgram(program, flags); - if (programObject->readsBaseVertexBuiltin) { + if (!(aspects & DrawSetupAspect::IndexBuffer) && programObject->readsBaseVertexBuiltin) { flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex; programObject = &m_programFactory->GetOrCreateProgram(program, flags); } @@ -48,13 +84,99 @@ const auto& programObj = *programObject; const auto vaoHandle = state.BoundVertexElements; if (MG_Pipe::MGPipeHandleIsNull(vaoHandle)) MagmaWireFatal("vertex-elements-record"); - if (!MG_Pipe::MGPipeHandleIsNull(vaoHandle)) { - if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record"); - const auto& vao = state.VertexElementsCsos[vaoHandle.Slot]; - if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation"); - for (Uint32 i = 0; i < vao.AttributeCount; ++i) - if (vao.Attributes[i].Enabled && (programObj.activeVertexInputLocationMask & (1u << i))) - WireBufferLegacyFatal(); + if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record"); + const auto& vao = state.VertexElementsCsos[vaoHandle.Slot]; + if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation"); + VertexInputStateFactory::BackendVertexInputState input{}; + if (!m_vertexInputStateFactory->BuildWireVertexInput(vao, state, + programObj.activeVertexInputLocationMask, input)) MagmaWireFatal("vertex-layout@P7"); + Vector vertexSlices(input.bindings.size()); + for (SizeT binding = 0; binding < input.bindings.size(); ++binding) { + const Uint32 location = input.bindingAttributeLocations[binding]; + const auto& attr = vao.Attributes[location]; + const auto& resource = state.VertexBuffers[location]; + if (MG_Pipe::MGPipeHandleIsNull(resource.Res)) MagmaWireFatal("client-vertex-array@P8"); + BufferSlice slice{}; + if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice)) + MagmaWireFatal("vertex-buffer-resource"); + const VkDeviceSize offset = input.bindingBaseOffsets[binding]; + const auto type = static_cast(attr.Type); + const SizeT elementSize = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra); + if (offset > slice.size || elementSize > slice.size - offset) return false; + const auto conversion = input.bindingConversions[binding]; + if (conversion != VertexInputStateFactory::VertexStreamConversion::None) { + const SizeT stride = static_cast(attr.Stride); + SizeT count = stride ? 1 + (static_cast(slice.size - offset) - elementSize) / stride : 1; + // Direct non-indexed draws bound the conversion; indexed/multi-draw may + // name arbitrary vertices, so preserve the entire valid source stream. + if (stride && !(aspects & DrawSetupAspect::IndexBuffer) && !resource.Divisor) + count = std::min(count, static_cast(drawParams.firstVertex) + drawParams.vertexCount); + if (!count) return false; + Vector source((count - 1) * stride + elementSize); + if (!m_bufferManager.ReadWireBuffer(resource.Res, offset, source.size(), source.data())) return false; + Vector converted; + if (!ConvertWireVertexStream(attr, conversion, source.data(), stride, count, converted)) + MagmaWireFatal("vertex-format-conversion@P7"); + if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), + converted.data(), converted.size(), 16, vertexSlices[binding])) return false; + } else { + slice.offset += offset; + slice.size -= offset; + vertexSlices[binding] = slice; + } + } + + BufferSlice indexSlice{}; + VkIndexType indexType = VK_INDEX_TYPE_MAX_ENUM; + if (aspects & DrawSetupAspect::IndexBuffer) { + if (!indices) return false; + const SizeT width = MG_Util::GetGLTypeSize(indices->indexType); + if (width != 1 && width != 2 && width != 4) return false; + indexType = width == 4 ? VK_INDEX_TYPE_UINT32 : width == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT8; + const auto& restart = MGB_CTX->GetRenderStateParameters(); + const Uint32 fixed = MG_Util::FixedRestartIndexForGLType(indices->indexType); + const Bool replaceRestart = restart.PrimitiveRestartEnabled && !restart.PrimitiveRestartFixedIndexEnabled && + restart.PrimitiveRestartIndex < fixed; + const Bool expandByte = width == 1 && !m_indexTypeUint8ExtensionEnabled; + const Bool cpuIndices = indices->forceClientMemory || MG_Pipe::MGPipeHandleIsNull(state.IndexBuffer.Res); + Vector copied; + const void* bytes = nullptr; + if (cpuIndices) bytes = reinterpret_cast(indices->indexByteOffset); + else { + if (!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, indexSlice)) + MagmaWireFatal("index-buffer-resource"); + const Uint64 offset = state.IndexBuffer.Offset + indices->indexByteOffset; + if (offset < state.IndexBuffer.Offset || offset > indexSlice.size || + indices->indexByteSize > indexSlice.size - offset) return false; + if (replaceRestart || expandByte || offset % width) { + copied.resize(indices->indexByteSize); + if (!m_bufferManager.ReadWireBuffer(state.IndexBuffer.Res, offset, copied.size(), copied.data())) return false; + bytes = copied.data(); + } + indexSlice.offset += offset; + indexSlice.size = indices->indexByteSize; + } + if (bytes) { + Vector rewritten; + SizeT uploadSize = indices->indexByteSize; + if (replaceRestart || expandByte) { + const SizeT outWidth = expandByte ? 2 : width; + rewritten.resize(uploadSize / width * outWidth); + for (SizeT i = 0; i < uploadSize / width; ++i) { + Uint32 value = 0; + Memcpy(&value, static_cast(bytes) + i * width, width); + if (replaceRestart && value == restart.PrimitiveRestartIndex) value = fixed; + if (expandByte && ResolvePrimitiveRestartEnable(aspects, indices) && value == fixed) value = 0xffffu; + Memcpy(rewritten.data() + i * outWidth, &value, outWidth); + } + bytes = rewritten.data(); + uploadSize = rewritten.size(); + if (expandByte) indexType = VK_INDEX_TYPE_UINT16; + } + if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), + bytes, uploadSize, 16, indexSlice)) return false; + } + if (!indexSlice.IsValid()) return false; } if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); @@ -150,12 +272,15 @@ pass.extent = {static_cast(extent.width), static_cast(extent.height)}; pass.layers = layers; pass.hash = pass.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash); - VertexInputStateFactory::BackendVertexInputState input{}; - const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass, false); + const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass, + ResolvePrimitiveRestartEnable(aspects, indices)); if (pipeline == VK_NULL_HANDLE) return false; - Uint32 binding = 0; + for (Uint32 i = 0; i < vertexSlices.size(); ++i) + vkCmdBindVertexBuffers(frame.commandBuffer, i, 1, &vertexSlices[i].buffer, &vertexSlices[i].offset); + Uint32 binding = static_cast(vertexSlices.size()); for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) { - if (!(programObj.activeVertexInputLocationMask & (1u << location))) continue; + if (!(programObj.activeVertexInputLocationMask & (1u << location)) || + (input.attributeLocationMask & (1u << location))) continue; VkFormat format = VK_FORMAT_UNDEFINED; const void* data = nullptr; VkDeviceSize size = 0; @@ -166,19 +291,12 @@ data, size, 16, slice)) return false; vkCmdBindVertexBuffers(frame.commandBuffer, binding++, 1, &slice.buffer, &slice.offset); } - if (indices) { - BufferSlice slice{}; - if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), - reinterpret_cast(indices->indexByteOffset), indices->indexByteSize, 16, slice)) return false; - const VkIndexType type = indices->indexType == GL_UNSIGNED_INT ? VK_INDEX_TYPE_UINT32 : - indices->indexType == GL_UNSIGNED_SHORT ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT8; - vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, slice.offset, type); - } + if (indexSlice.IsValid()) vkCmdBindIndexBuffer(frame.commandBuffer, indexSlice.buffer, indexSlice.offset, indexType); if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) return false; vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); ApplyDynamicDrawStateTail(frame, pass.extent, fbo->IsDefault, ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin)); - (void)drawParams; + if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1); return true; } diff --git a/MobileGL/MG_Backend/Init.cpp b/MobileGL/MG_Backend/Init.cpp index e19b7fea..3730bba8 100644 --- a/MobileGL/MG_Backend/Init.cpp +++ b/MobileGL/MG_Backend/Init.cpp @@ -113,8 +113,7 @@ namespace MobileGL::MG_Backend { switch (type) { case BackendType::DirectGLES: return MG_Pipe::kMGPipeSubsystemsMigratedAtP5e; case BackendType::DirectVulkan: - return (MG_Pipe::kMGPipeSubsystemsMigratedAtP4a | MG_Pipe::kMGPipeSubsystemBufferBindings) & - ~MG_Pipe::kMGPipeSubsystemResources; + return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a | MG_Pipe::kMGPipeSubsystemBufferBindings; default: return 0; } }