[MG_Backend] (Disaggregated): consume Magma application buffers from server resources

This commit is contained in:
2026-09-20 02:37:02 -04:00
parent f564d210cd
commit fbb7873fbb
12 changed files with 926 additions and 84 deletions
@@ -365,6 +365,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void BackendObject_DirectVulkan::Initialize() {
#if MOBILEGL_BUILD_DISAGGREGATED
VkBufferManager::RegisterWireResourceOps();
#endif
m_initialized = true;
}
+102 -10
View File
@@ -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<Uint64>(countOffset), sizeof(count), &count)) return;
drawcount = static_cast<GLsizei>(std::min<Uint32>(count, static_cast<Uint32>(drawcount)));
if (!drawcount) return;
}
const SizeT commandSize = indexed ? sizeof(DrawElementsIndirectCommand) : sizeof(DrawArraysIndirectCommand);
if (stride == 0) stride = static_cast<GLsizei>(commandSize);
if (stride < static_cast<GLsizei>(commandSize)) return;
const Uint64 byteCount = static_cast<Uint64>(stride) * (drawcount - 1) + commandSize;
if (byteCount > std::numeric_limits<SizeT>::max()) return;
Vector<Uint8> bytes(static_cast<SizeT>(byteCount));
if (!buffers.ReadWireBuffer(state.VerbIndirectBuffer, reinterpret_cast<Uint64>(indirect),
byteCount, bytes.data())) return;
if (indexed) {
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize != 1 && indexSize != 2 && indexSize != 4) return;
Vector<DrawIndexedCmdParam> params(static_cast<SizeT>(drawcount));
MultiDrawIndexedCmd payload{};
payload.mode = mode;
payload.indexBufferView.indexType = type;
payload.drawCount = static_cast<Uint32>(drawcount);
payload.pParams = params.data();
for (GLsizei i = 0; i < drawcount; ++i) {
DrawElementsIndirectCommand command{};
Memcpy(&command, bytes.data() + static_cast<SizeT>(i) * stride, sizeof(command));
params[i] = {command.count, command.instanceCount, command.firstIndex,
command.baseVertex, command.baseInstance};
const Uint64 end = (static_cast<Uint64>(command.firstIndex) + command.count) * indexSize;
if (end > std::numeric_limits<SizeT>::max()) return;
payload.indexBufferView.indexByteSize = std::max<SizeT>(payload.indexBufferView.indexByteSize,
static_cast<SizeT>(end));
}
pVulkanRenderer->MultiDrawElements(payload);
} else {
Vector<DrawCmdParam> params(static_cast<SizeT>(drawcount));
for (GLsizei i = 0; i < drawcount; ++i) {
DrawArraysIndirectCommand command{};
Memcpy(&command, bytes.data() + static_cast<SizeT>(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<Uint32>(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<Uint64>(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;
@@ -7,6 +7,9 @@
// End of Source File Header
#include "BufferArena.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#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;
}
@@ -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<VkDeviceSize>(offset);
const VkDeviceSize remaining = bufferSize - start;
size = declaredSize == MG_Pipe::kMGPipeWholeBuffer
? remaining : std::min<VkDeviceSize>(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<VkDeviceSize>(1, minUniformBufferOffsetAlignment);
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(m_physicalDevice, &properties);
m_wireStorageOffsetAlignment = std::max<VkDeviceSize>(1, properties.limits.minStorageBufferOffsetAlignment);
m_wireTexelOffsetAlignment = std::max<VkDeviceSize>(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<Uint>(location));
const auto& state = MG_Pipe::MGPipeApplier();
if (unit < 0 || static_cast<Uint32>(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<Uint8>(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<TextureInternalFormat>(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<VkDeviceSize>(m_wireMaxTexelElements)) * texelSize;
if (size == 0) return false;
if (start > std::numeric_limits<VkDeviceSize>::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<Uint>(location));
const auto& state = MG_Pipe::MGPipeApplier();
if (unit < 0 || static_cast<Uint32>(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<Uint>(location));
const auto& state = MG_Pipe::MGPipeApplier();
if (unit < 0 || static_cast<Uint32>(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_<N> 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<VkDeviceSize>::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<VkDeviceSize>::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<Uint32>::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<Uint8> zero;
zero.assign(static_cast<SizeT>(blockSize), 0);
BufferSlice padded{};
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, m_wireFrameIndex, zero.data(), blockSize,
std::max<VkDeviceSize>(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<Uint32>::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<Uint32>(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<Uint32>(blockIndex) >= activeUniformBlockCount))
WireDescriptorFatal("uniform-buffer-reflection-block");
#endif
const Uint32 frontendBinding = program.GetUniformBlockBinding(static_cast<Uint32>(blockIndex));
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire())
return ResolveWireUniformBufferPayload(program, static_cast<Uint32>(blockIndex), frontendBinding, out);
#endif
const Uint32 uniformBindingPointCount =
static_cast<Uint32>(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);
@@ -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;
@@ -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<DataType>(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<Uint32>(attr.Stride) % alignment))
conversion = VertexStreamConversion::Repack;
Uint32 stride = static_cast<Uint32>(attr.Stride);
if (stride && conversion != VertexStreamConversion::None)
stride = conversion == VertexStreamConversion::Repack ? static_cast<Uint32>(elementSize)
: static_cast<Uint32>(attr.Size) * sizeof(Float);
const Uint32 binding = static_cast<Uint32>(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<SizeT>(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<Uint32>(out.bindings.size());
out.state.pVertexBindingDescriptions = out.bindings.data();
out.state.vertexAttributeDescriptionCount = static_cast<Uint32>(out.attributes.size());
out.state.pVertexAttributeDescriptions = out.attributes.data();
if (!out.bindingDivisors.empty()) {
out.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(out.bindingDivisors.size());
out.divisorState.pVertexBindingDivisors = out.bindingDivisors.data();
out.state.pNext = &out.divisorState;
}
return true;
}
#endif
void VertexInputStateFactory::OnFrameBoundary() {
++m_frameBoundaryCounter;
@@ -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 <MG_Pipe/MGPipeHandles.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeApply.h>
#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,
@@ -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 <Config.h>
#include <cstdlib>
#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<unsigned long long>(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<SizeT>(offset),
static_cast<SizeT>(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<SizeT>(offset),
static_cast<SizeT>(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<SizeT>(offset), static_cast<SizeT>(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<const Uint8*>(resource->buffer.GetMappedData()) + offset, static_cast<SizeT>(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, &copy);
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<Uint8> bytes(static_cast<SizeT>(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<Uint64>(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<VkDeviceSize>(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<Uint64>(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<VkDeviceSize>(offset);
region.size = static_cast<VkDeviceSize>(size);
vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, &region);
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) {
@@ -13,6 +13,10 @@
#include "../VkIncludes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include "MG_Pipe/MGPipeTypes.h"
#include <unordered_map>
#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<Range1D> stagedCoverage;
};
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
return (static_cast<Uint64>(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<MG_State::GLState::BufferObject>& 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<Vector<VkBufferObject>> m_deferredBufferReleases;
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
Vector<WeakPtr<VkBufferResource>> m_liveResources;
#if MOBILEGL_BUILD_DISAGGREGATED
std::unordered_map<Uint64, WireBufferResource> 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;
@@ -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
@@ -16,6 +16,42 @@
std::abort();
}
static Bool ConvertWireVertexStream(const MGPVertexAttribWire& attr,
VertexInputStateFactory::VertexStreamConversion conversion, const Uint8* source,
SizeT stride, SizeT count, Vector<Uint8>& output) {
using Conversion = VertexInputStateFactory::VertexStreamConversion;
const auto type = static_cast<DataType>(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 = [&]<typename T>() {
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<Float>(value);
Memcpy(output.data() + (i * attr.Size + component) * sizeof(Float), &converted, sizeof(Float));
}
}
return true;
};
switch (type) {
case DataType::Float64: return convert.template operator()<Double>();
case DataType::Int8: return convert.template operator()<Int8>();
case DataType::Uint8: return convert.template operator()<Uint8>();
case DataType::Int16: return convert.template operator()<Int16>();
case DataType::Uint16: return convert.template operator()<Uint16>();
case DataType::Int32: return convert.template operator()<Int32>();
case DataType::Uint32: return convert.template operator()<Uint32>();
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<BufferSlice> 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<DataType>(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<SizeT>(attr.Stride);
SizeT count = stride ? 1 + (static_cast<SizeT>(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<SizeT>(count, static_cast<SizeT>(drawParams.firstVertex) + drawParams.vertexCount);
if (!count) return false;
Vector<Uint8> source((count - 1) * stride + elementSize);
if (!m_bufferManager.ReadWireBuffer(resource.Res, offset, source.size(), source.data())) return false;
Vector<Uint8> 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<Uint8> copied;
const void* bytes = nullptr;
if (cpuIndices) bytes = reinterpret_cast<const void*>(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<Uint8> 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<const Uint8*>(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<Int>(extent.width), static_cast<Int>(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<Uint32>(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<const void*>(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;
}
+1 -2
View File
@@ -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;
}
}