Files
MobileGL/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp
T
swung0x48 98c8e8833c [Refactor] (CMakeLists.txt, MG_Backend/DirectVulkan): split VulkanRenderer.cpp and its fragments by area
- code moves unchanged; helpers several files use are in VulkanRendererInternal.h
2026-10-16 04:28:47 -04:00

767 lines
42 KiB
C++

// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
#if defined(__APPLE__)
namespace {
void SendVoid(id receiver, const char* selector) {
ObjcMsgSend<void (*)(id, SEL)>()(receiver, sel_registerName(selector));
}
void Release(id object) {
if (object) {
SendVoid(object, "release");
}
}
} // namespace
#endif
static Bool IsPowerVRDevice(const VkPhysicalDeviceProperties& properties) {
return std::strstr(properties.deviceName, "PowerVR") != nullptr;
}
// ---- D12.3: DynamicTailKey's inputs against the P2 chunk table ----
//
// DynamicTailKey's inventory (declared above, one line per reader) is an exact,
// hand-maintained enumeration of what the six Apply* in the tail read. The P2 chunk table
// (MG_Pipe/MGPipeRenderStateSpans.h) is an independent, offsetof-derived statement of
// which bytes of RenderStateParameters are dynamic state. The two were written for
// different reasons, so making them check each other is free evidence: if a later chunk
// edit demotes or promotes one of these members, the mismatch is a BUILD BREAK here rather
// than a tail that silently stops being re-run when its input moves.
//
// The brief (P2 D12.3) expects every input to be dynamic; the tree says otherwise for
// exactly one, and the tree is right - see the ScissorTestEnabledMask note below.
//
// These assertions ARE D19's DynamicChunksCoverMagmasDynamicTailKey, in the only file this
// package owns. D19 names it as a case in MG_Test/Pipe/RenderStateSpansTest.cpp, which
// belongs to package A (C.5). INTEGRATOR: make sure the outcome is not "neither" - if
// package A did not land that case, this static_assert block is the whole gate, and if it
// did, the two are redundant on purpose and both should stay.
namespace {
// Is [begin, begin + size) covered entirely by DYNAMIC chunks?
constexpr Bool MagmaRenderStateRangeIsDynamic(SizeT begin, SizeT size) {
const SizeT end = begin + size;
for (SizeT i = 0; i < MG_Pipe::kMGPipeRenderStateChunkCount; ++i) {
const SizeT chunkBegin = MG_Pipe::kMGPipeRenderStateChunkBoundaries[i];
const SizeT chunkEnd = MG_Pipe::kMGPipeRenderStateChunkBoundaries[i + 1];
if (end <= chunkBegin || begin >= chunkEnd) continue; // disjoint
if (MG_Pipe::MGPipeRenderStateChunkIsPipeline(i)) return false;
}
return true;
}
using MagmaTailRsp = RenderStateParameters;
#define MAGMA_TAIL_INPUT_IS_DYNAMIC(Member) \
static_assert(MagmaRenderStateRangeIsDynamic(offsetof(MagmaTailRsp, Member), \
sizeof(MagmaTailRsp::Member)), \
"ApplyDynamicDrawStateTail reads " #Member \
", which the P2 chunk table no longer calls dynamic state: a change to it would " \
"move the pipeline version, not the parameters version, and the tail would stop " \
"being re-run for it")
// Viewports, DepthRanges and ScissorBoxes are asserted over the WHOLE array while the
// tail reads only element 0. That is deliberately stricter than the reader needs: the
// chunk table has no per-element granularity today, so an array that is dynamic at all
// is dynamic entirely, and asserting the whole of it says so. If a later phase ever
// splits a per-viewport chunk out, this is a build break by design - narrow the assert
// to element 0 then, and say why in the same commit.
MAGMA_TAIL_INPUT_IS_DYNAMIC(Viewports); // ApplyGLViewportState: Viewports[0]
MAGMA_TAIL_INPUT_IS_DYNAMIC(DepthRanges); // ApplyGLViewportState: DepthRanges[0]
MAGMA_TAIL_INPUT_IS_DYNAMIC(BlendColor); // ApplyBlendConstants
MAGMA_TAIL_INPUT_IS_DYNAMIC(PolygonOffsetFactor); // ApplyPolygonOffsetState
MAGMA_TAIL_INPUT_IS_DYNAMIC(PolygonOffsetUnits); // ApplyPolygonOffsetState
MAGMA_TAIL_INPUT_IS_DYNAMIC(PolygonOffsetClamp); // ApplyPolygonOffsetState
MAGMA_TAIL_INPUT_IS_DYNAMIC(LineWidth); // ApplyLineWidthState
MAGMA_TAIL_INPUT_IS_DYNAMIC(ScissorBoxes); // the scissor rect: ScissorBoxes[0]
#undef MAGMA_TAIL_INPUT_IS_DYNAMIC
// ApplyStencilState reads three of the seven members of each face, and D6 splits
// StencilFaceState at sub-member granularity for exactly this reason: Ref, ValueMask
// and WriteMask are VK_DYNAMIC_STATE_STENCIL_{REFERENCE,COMPARE_MASK,WRITE_MASK}, while
// Func and the three ops are baked into the pipeline. Asserted per member, per face,
// because the split runs THROUGH the struct rather than around it.
constexpr SizeT kMagmaStencilFace1 = offsetof(MagmaTailRsp, StencilStates) + sizeof(StencilFaceState);
#define MAGMA_TAIL_STENCIL_IS_DYNAMIC(Member) \
static_assert(MagmaRenderStateRangeIsDynamic(offsetof(MagmaTailRsp, StencilStates) + \
offsetof(StencilFaceState, Member), \
sizeof(StencilFaceState::Member)), \
"ApplyStencilState reads the FRONT face's " #Member " as dynamic state"); \
static_assert(MagmaRenderStateRangeIsDynamic(kMagmaStencilFace1 + offsetof(StencilFaceState, Member), \
sizeof(StencilFaceState::Member)), \
"ApplyStencilState reads the BACK face's " #Member " as dynamic state")
MAGMA_TAIL_STENCIL_IS_DYNAMIC(Ref);
MAGMA_TAIL_STENCIL_IS_DYNAMIC(ValueMask);
MAGMA_TAIL_STENCIL_IS_DYNAMIC(WriteMask);
#undef MAGMA_TAIL_STENCIL_IS_DYNAMIC
// THE ONE INPUT THAT IS NOT DYNAMIC, and the brief's D12.3 says it should be.
// The tree wins, and it is right: the split's only rule is "a byte is pipeline state
// iff a public setter that calls BumpVersions() writes it", and ScissorTestEnabledMask
// is written by SetCapability(ScissorTest), which does. It sits in pipeline chunk P6
// with the other capability bools. The tail reads it only to decide between the
// scissor box and a full-extent rect, and it is HARMLESS there for a reason worth
// stating: a pipeline-half write moves the pipeline version, and the pipeline version
// moves only together with the parameters version (BumpVersions bumps both), so the
// tail's version gate is invalidated by it just the same. A DYNAMIC member promoted
// into the pipeline half would break that direction, which is what the asserts above
// are for; this one is pinned in the opposite direction so that DEMOTING it - which
// would be a real G7 violation - is also a build break.
static_assert(!MagmaRenderStateRangeIsDynamic(offsetof(MagmaTailRsp, ScissorTestEnabledMask),
sizeof(MagmaTailRsp::ScissorTestEnabledMask)),
"ScissorTestEnabledMask is written by SetCapability(ScissorTest), which calls "
"BumpVersions(), so the chunk table must keep it in the pipeline half");
} // namespace
namespace {
static constexpr Uint32 kDescriptorSetsPerFrame = 64;
struct BlitImageBinding {
VkImage image = VK_NULL_HANDLE;
VkImageLayout* trackedLayout = nullptr;
VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_NONE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
IntVec2 extent = {0, 0};
Uint32 mipLevel = 0;
Uint32 mipLevelCount = 1;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
// z slice for a VK_IMAGE_TYPE_3D source; array attachments use baseArrayLayer instead.
Uint32 depthOffset = 0;
const char* label = nullptr;
};
static Uint32 ComputeMaxProgramBindings(const VkPhysicalDeviceProperties& properties,
const ProgramFactory::UpdateAfterBindLimits& updateAfterBindLimits) {
const auto& limits = properties.limits;
static constexpr Uint32 kMinProgramBindings = 16;
static constexpr Uint32 kMaxProgramBindingsCap = 256;
const Uint32 maxCombinedImageSamplers =
std::min(limits.maxPerStageDescriptorSamplers, limits.maxDescriptorSetSamplers);
const Uint32 maxSampledImages =
std::min(limits.maxPerStageDescriptorSampledImages, limits.maxDescriptorSetSampledImages);
const Uint32 maxDynamicUniformBuffers =
std::min(limits.maxPerStageDescriptorUniformBuffers, limits.maxDescriptorSetUniformBuffersDynamic);
Uint32 maxBindings = limits.maxPerStageResources;
maxBindings = std::min(maxBindings, maxCombinedImageSamplers);
maxBindings = std::min(maxBindings, maxSampledImages + maxDynamicUniformBuffers);
if (updateAfterBindLimits.enabled) {
const Uint32 updateAfterBindSamplers = std::min(updateAfterBindLimits.maxPerStageSamplers,
updateAfterBindLimits.maxSetSamplers);
const Uint32 updateAfterBindSampledImages = std::min(updateAfterBindLimits.maxPerStageSampledImages,
updateAfterBindLimits.maxSetSampledImages);
const Uint32 updateAfterBindDynamicUniformBuffers =
std::min(updateAfterBindLimits.maxPerStageUniformBuffers,
updateAfterBindLimits.maxSetUniformBuffersDynamic);
Uint32 updateAfterBindBindings = updateAfterBindLimits.maxPerStageResources;
updateAfterBindBindings = std::min(updateAfterBindBindings, updateAfterBindSamplers);
updateAfterBindBindings =
std::min(updateAfterBindBindings, updateAfterBindSampledImages + updateAfterBindDynamicUniformBuffers);
maxBindings = std::max(maxBindings, updateAfterBindBindings);
}
maxBindings = std::max(kMinProgramBindings, maxBindings);
maxBindings = std::min(kMaxProgramBindingsCap, maxBindings);
return maxBindings;
}
} // namespace
VkBool32 VulkanRenderer::DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) {
auto typeToString = [](VkDebugUtilsMessageTypeFlagsEXT messageType) {
switch (messageType) {
case VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT:
return "General";
case VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT:
return "Validation";
case VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT:
return "Performance";
case VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT:
return "DeviceAddressBinding";
default:
return "Other";
}
};
switch (messageSeverity) {
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:
MGLOG_E_ONCE("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage);
break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
MGLOG_W_ONCE("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage);
break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT:
MGLOG_D("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage);
break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT:
MGLOG_D("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage);
break;
default:
break;
}
return VK_FALSE;
}
VulkanRenderer::VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg)
: m_window(window), m_config(cfg), m_presentsToAppWindow(window != NativeWindowType{}) {
if (!m_presentsToAppWindow) {
m_config.SwapInterval.reset();
}
// Initialize();
}
VulkanRenderer::~VulkanRenderer() {
Shutdown();
}
void VulkanRenderer::Initialize() {
// P2 D14, and it belongs HERE rather than on a draw: "a Track-H subsystem whose bit is
// clear is a STARTUP Fatal{PipeLegacyMemosDisabled}". Checks Magma's own bit only, and
// only once this backend is the one being brought up, so an Espryt-side bitmask cannot
// kill a Magma run and vice versa.
MagmaPipeValidateSubsystemConfiguration();
CreateInstance();
if (!CreateSurface()) throw RuntimeError("DirectVulkan: the initial surface could not be created");
PickPhysicalDevice();
CreateLogicalDeviceAndQueues();
CreateAllocator();
CreateCommandPool();
m_progressMarkers.Init(m_device, m_physicalDevice.queueFamilies.graphicsFamily);
// Frames-in-flight is a request, not a guarantee: it also seeds the swapchain image
// count (SwapchainObject clamps the hint into [minImageCount, maxImageCount]). Not every
// driver/surface supports >= 3 swapchain images, and keeping more frame slots than the
// surface can present would leave the surplus slots stalling on vkAcquireNextImageKHR.
// So clamp to the surface's real limits here, before any per-frame resource is sized off
// it. (The standalone driver POST is headless and has no surface, so this check lives at
// renderer init.) Existing logs already report the swapchain's min/actual image count;
// this one adds the frames-in-flight decision itself.
{
// Desired depth comes from MOBILEGL_MAGMA_FRAMESINFLIGHT, parsed once by ConfigLoader
// with a default of 3 when the variable is unset or invalid.
Uint32 requestedFramesInFlight = MG_Config::Features.FramesInFlight;
MGLOG_I("MaxFramesInFlight: configured request=%u", requestedFramesInFlight);
VkSurfaceCapabilitiesKHR surfaceCaps{};
const VkResult capsResult = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(
m_physicalDevice.handle, m_surface, &surfaceCaps);
if (capsResult != VK_SUCCESS) {
MGLOG_W("MaxFramesInFlight: vkGetPhysicalDeviceSurfaceCapabilitiesKHR failed (VkResult=%d); "
"keeping requested %u", static_cast<Int>(capsResult), requestedFramesInFlight);
} else {
// Frames-in-flight is the CPU pipeline depth; it only needs to stay <= the number
// of swapchain images the surface can provide (maxImageCount), so the extra slots
// never stall on vkAcquireNextImageKHR. It must NOT be forced up to minImageCount:
// the swapchain independently gets >= minImageCount images (SwapchainObject raises
// the count), and inflating the CPU depth would only add latency + memory.
Uint32 chosenFramesInFlight = requestedFramesInFlight;
if (surfaceCaps.maxImageCount != 0 && chosenFramesInFlight > surfaceCaps.maxImageCount) {
chosenFramesInFlight = surfaceCaps.maxImageCount; // 0 == no upper bound
}
if (chosenFramesInFlight < 2) {
chosenFramesInFlight = 2; // never drop below double buffering
}
m_config.MaxFramesInFlight = chosenFramesInFlight;
if (chosenFramesInFlight != requestedFramesInFlight) {
MGLOG_W("MaxFramesInFlight: requested %u unsupported by surface (minImageCount=%u, "
"maxImageCount=%u); using %u", requestedFramesInFlight, surfaceCaps.minImageCount,
surfaceCaps.maxImageCount, chosenFramesInFlight);
} else {
MGLOG_I("MaxFramesInFlight: using %u (surface minImageCount=%u, maxImageCount=%u)",
chosenFramesInFlight, surfaceCaps.minImageCount, surfaceCaps.maxImageCount);
}
}
}
VK_VERIFY(m_frameContext.Initialize(m_device, m_commandPool, m_config.MaxFramesInFlight),
"CreateFrameContexts");
MGLOG_I("CreateFrameContexts completed");
auto succeeded = false;
succeeded = m_bufferManager.Initialize({
.allocator = m_allocator,
.frameCount = m_frameContext.GetFrameCount(),
.minUploadBytes = 4 * 1024 * 1024,
.transientMemoryUsage = VMA_MEMORY_USAGE_AUTO,
.transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT,
.transientPersistentMapping = true,
.transformFeedbackUsageEnabled = m_transformFeedbackFeatureEnabled,
});
MOBILEGL_ASSERT(succeeded, "VkBufferManager initialization failed.");
m_bufferManager.SetCopyCommandProvider(this);
m_bufferManager.SetSubmitCounterSource(&m_submitCounter);
if (m_timerQuerySupported) {
m_timerQueryManager = MakeUnique<VkTimerQueryManager>();
if (m_timerQueryManager->Initialize({.device = m_device,
.frameCount = m_frameContext.GetFrameCount(),
.timestampValidBits = m_timestampValidBits,
.timestampPeriodNs = m_timestampPeriodNs})) {
m_frameContext.SetRecordingObserver(this);
} else {
MGLOG_W("VkTimerQueryManager initialization failed; timer queries disabled");
m_timerQueryManager.reset();
m_timerQuerySupported = false;
}
}
m_textureManager = MakeUnique<VkTextureManager>();
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
succeeded = m_textureManager->Initialize(
{m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue,
m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled,
m_sampledReadStageMask,
static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily)});
MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed.");
m_textureManager->SetWireE5RawCopyKit(GetWireE5RawCopyKit());
m_renderPassManager =
MakeUnique<VkRenderPassManager>(m_device, m_physicalDevice.handle, m_allocator, m_config,
*m_textureManager, m_swapchainObject);
MOBILEGL_ASSERT(m_renderPassManager != nullptr, "VkRenderPassManager creation failed.");
succeeded = m_renderPassManager->Initialize();
MOBILEGL_ASSERT(succeeded, "VkRenderPassManager initialization failed.");
const Uint32 maxProgramBindings = ComputeMaxProgramBindings(m_physicalDevice.properties, m_updateAfterBindLimits);
MGLOG_I("DirectVulkan: using %u program descriptor bindings", maxProgramBindings);
if (IsPowerVRDevice(m_physicalDevice.properties)) {
m_config.DisablePipelineCache = true;
MGLOG_W("DirectVulkan: disabling pipeline cache on PowerVR device %s",
m_physicalDevice.properties.deviceName);
}
RecreateSwapchain();
m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config);
MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed.");
{
// Qualcomm's pipeline compiler does not keep vertex positions invariant across
// the pipelines of a multi-pass depth-equality chain (even with the SPIR-V
// Invariant decoration), so a blended depth-writing prepass makes later
// equality-compare passes drop whole primitives (MC 26.3 improved-transparency
// clouds flicker black). Suppress depth writes on accumulation-blended pipelines
// there (see PipelineFactory::ShouldSuppressDepthWrite for the exact scope);
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE forces the quirk on or off on any
// driver.
const MG_Config::QuirkOverride quirkOverride =
MG_Config::Features.MagmaDisableBlendedDepthWriteQuirk;
const Bool suppressBlendedDepthWrite = PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(
quirkOverride, m_physicalDevice.properties.vendorID);
if (suppressBlendedDepthWrite) {
MGLOG_I("DirectVulkan: suppressing depth writes on accumulation-blended pipelines "
"(driver lacks cross-pipeline position invariance)%s",
quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
}
PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite);
}
ProgramFactory::SubgroupLoweringPolicy subgroupPolicy{};
subgroupPolicy.emulateSubgroups = ShouldEmulateSubgroups(m_nativeSubgroupSupported);
subgroupPolicy.fixIterationRPSubgroupScratch =
UsesNativeSubgroups(m_nativeSubgroupSupported) && ShouldApplyIterationRPFixes();
subgroupPolicy.fixIterationRPBarrier = ShouldApplyIterationRPFixes();
subgroupPolicy.deriveNumSubgroups = UsesNativeSubgroups(m_nativeSubgroupSupported);
subgroupPolicy.requireFullSubgroups = m_computeFullSubgroupsFeatureEnabled;
subgroupPolicy.nativeSubgroupSize = m_nativeSubgroupSize;
subgroupPolicy.maxComputeWorkgroupSubgroups = m_maxComputeWorkgroupSubgroups;
subgroupPolicy.maxComputeSharedMemoryBytes =
m_physicalDevice.properties.limits.maxComputeSharedMemorySize;
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
m_shaderDrawParametersFeatureEnabled,
m_unformattedFloatStorageImagesEnabled,
m_tessellationAndGeometryPointSizeFeatureEnabled,
MG_Config::Features.EnableSpirvValidation,
m_updateAfterBindLimits, subgroupPolicy);
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
// The swapchain already exists at this point (Initialize creates it first), so seed the
// height the factory could not be told about from CreateSwapchain.
m_programFactory->SetDefaultFramebufferHeight(m_swapchainObject.GetExtent().height);
// Aging evictions (render passes and program entries) must purge the dependent
// pipeline / compute-pipeline / descriptor-set caches in the same step; both
// sweeps only run from the frame-boundary seams, long after initialization.
m_programFactory->SetEvictionObserver(this);
m_samplerManager = MakeUnique<VkSamplerManager>();
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
succeeded = m_samplerManager->Initialize({m_device, &m_config, m_samplerAnisotropyFeatureEnabled,
m_physicalDevice.properties.limits.maxSamplerAnisotropy,
m_customBorderColorFeatureEnabled,
m_maxCustomBorderColorSamplers});
MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed.");
succeeded = InitializeBlitResources();
MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed.");
succeeded = InitializeDepthMipmapResources();
MOBILEGL_ASSERT(succeeded, "Depth mipmap pipeline resource initialization failed.");
m_uniformManager = MakeUnique<UniformManager>();
MOBILEGL_ASSERT(m_uniformManager != nullptr, "UniformDescriptorBinder creation failed.");
succeeded = m_uniformManager->Initialize(
m_device, m_physicalDevice.handle, &m_bufferManager, m_programFactory.get(),
m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight,
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed.");
m_uniformManager->SetWireInvalidStorageImageArm(m_wireNullDescriptor);
m_vertexInputStateFactory =
MakeUnique<VertexInputStateFactory>(m_config, m_physicalDevice.handle);
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
// Prime the first frame so Render() always targets an acquired swapchain image.
// A zero-area window (GLFW's hidden helper window during the WGL bootstrap, or a
// window that is already minimized) legitimately yields no swapchain here; defer
// the first acquire to Present in that case instead of acquiring from a null
// swapchain handle.
if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) {
VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR) {
// Nothing was acquired and no semaphore signal was armed, so
// rebuilding and re-acquiring on the same semaphore is safe.
MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult);
RecreateSwapchain();
acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
} else if (acquireResult == VK_SUBOPTIMAL_KHR) {
// The image is usable, and its acquire signal is already armed on
// imageAvailableSemaphore. Re-acquiring here would arm a second signal on a
// binary semaphore whose first one nobody has waited on yet; keep the image.
// Only a real surface change schedules a rebuild.
m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate();
acquireResult = VK_SUCCESS;
}
VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage");
// The first acquired image is the one the opening frame renders into, so the
// default framebuffer addresses it from the start (see
// m_defaultFramebufferImageIndex).
m_defaultFramebufferImageIndex = m_imageIndexAcquired;
} else {
MGLOG_W("DirectVulkan: no swapchain at initialization (zero-area window); deferring first acquire");
}
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
MGLOG_D("VulkanRenderer initialized");
}
void VulkanRenderer::Shutdown() {
if (m_instance == VK_NULL_HANDLE && m_device == VK_NULL_HANDLE && m_surface == VK_NULL_HANDLE) {
return;
}
if (m_device != VK_NULL_HANDLE) {
VK_VERIFY(vkDeviceWaitIdle(m_device));
}
m_progressMarkers.Shutdown();
// Parked surface targets (ActivateSurfaceTarget) go before the device and instance.
for (auto& [_, target] : m_parkedTargets) DestroyParkedTarget(target);
m_parkedTargets.clear();
// P7 wave 2 package B3: the session's decline tally. Silent when nothing declined, so a
// green lane stays quiet and a lane that dropped a draw cannot (rule I's observable).
WireDeclineTally::Dump("shutdown");
// The SPIR-V screen's engagement proof: a decline-free session above reads as "screened
// and valid" only with this count beside it (distinct modules, process-wide).
MGLOG_I("MGWIRE-VCONV[shutdown] converted=%llu reused=%llu renamedWrites=%llu",
static_cast<unsigned long long>(m_wireStreamsConverted),
static_cast<unsigned long long>(m_wireStreamsReused),
static_cast<unsigned long long>(m_bufferManager.GetWireRenameCount()));
MGLOG_I("MGWIRE-SPIRV-SCREEN[shutdown] modulesValidated=%llu",
static_cast<unsigned long long>(ProgramFactory::DriverModuleValidatorRuns()));
DestroyWireDrawPass();
CollectWireObjects(m_submitCounter, true);
ClearAllWireDrawPassCaches();
DestroyWireColorBlitResources();
#if MOBILEGL_BUILD_DISAGGREGATED
DestroyWireYuvResources();
#endif
DestroyWireDepthMipmapResources();
DestroyWireMultisampleResolveResources();
if (m_textureManager) m_textureManager->SetWireE5RawCopyKit(nullptr);
DestroyWireE5RawCopyKit(m_device, m_wireE5RawCopyKit);
m_wireE5RawReads = false;
#if MOBILEGL_BUILD_DISAGGREGATED
DestroySharedImagePresentTargets(false);
DestroySharedImageSync();
#endif
OnSubmitsCompletedUpTo(m_submitCounter);
DestroySubmitFencePool();
DestroyDeferredDepthMipmapCleanup();
DestroyMultisampleResolveScratchImage();
DestroyComputePipelines();
// No sweep runs during teardown, but the observers point at this renderer
// and the factories die at different times below; disconnect them first.
if (m_renderPassManager) {
}
if (m_programFactory) {
m_programFactory->SetEvictionObserver(nullptr);
}
m_pipelineFactory.reset();
ShutdownBlitResources();
ShutdownDepthMipmapResources();
if (m_samplerManager) {
m_samplerManager->Shutdown();
m_samplerManager.reset();
}
if (m_textureManager) {
m_textureManager->Shutdown();
m_textureManager.reset();
}
m_vertexInputStateFactory.reset();
m_xfbCounterBuffer.Destroy();
m_xfbCounterSlotOwner.fill(0);
m_xfbCounterSlotLastUse.fill(0);
m_xfbCounterSlotUseSerial = 0;
m_xfbCountersValid.fill(false);
m_xfbLastSeenGeneration.fill(0);
if (m_occlusionQueryPool != VK_NULL_HANDLE) {
vkDestroyQueryPool(m_device, m_occlusionQueryPool, nullptr);
m_occlusionQueryPool = VK_NULL_HANDLE;
}
if (m_xfbQueryPool != VK_NULL_HANDLE) {
vkDestroyQueryPool(m_device, m_xfbQueryPool, nullptr);
m_xfbQueryPool = VK_NULL_HANDLE;
}
if (m_primGenReroutePool != VK_NULL_HANDLE) {
vkDestroyQueryPool(m_device, m_primGenReroutePool, nullptr);
m_primGenReroutePool = VK_NULL_HANDLE;
}
m_primGenRerouteActiveSlots.clear();
m_primGenRerouteSlotCursor = 0;
m_primGenRerouteSlotOpen = false;
// Not sticky across renderers: the next bring-up re-decides both (from the
// per-process probe memo, so it re-decides without re-probing).
m_primGenRerouteKind = MG_Util::SelfTest::PrimGenRerouteKind::None;
m_primGenStreamCountsXfbInactiveDraws = false;
m_bufferManager.Shutdown();
// Device is idle (vkDeviceWaitIdle above); query pools can be destroyed.
m_frameContext.SetRecordingObserver(nullptr);
if (m_timerQueryManager) {
m_timerQueryManager->Shutdown();
m_timerQueryManager.reset();
}
if (m_device != VK_NULL_HANDLE) {
m_frameContext.Destroy(m_device, m_commandPool);
}
if (m_uniformManager) {
m_uniformManager->Shutdown();
m_uniformManager.reset();
}
m_programFactory.reset();
if (m_renderPassManager) {
ShutdownSwapchain();
} else if (m_device != VK_NULL_HANDLE) {
m_swapchainObject.Shutdown(m_device);
}
m_renderPassManager.reset();
if (m_commandPool != VK_NULL_HANDLE) {
vkDestroyCommandPool(m_device, m_commandPool, nullptr);
m_commandPool = VK_NULL_HANDLE;
}
DestroyAllocator();
if (m_device != VK_NULL_HANDLE) {
vkDestroyDevice(m_device, nullptr);
m_device = VK_NULL_HANDLE;
}
s_vkCmdDrawIndexedIndirectCount = nullptr;
s_vkCmdWireDrawIndirectCount = nullptr;
s_vkCmdDrawMultiEXT = nullptr;
s_vkCmdDrawMultiIndexedEXT = nullptr;
s_vkCmdSetFragmentShadingRateKHR = nullptr;
m_fragmentShadingRate = {};
if (m_instance != VK_NULL_HANDLE && m_surface != VK_NULL_HANDLE) {
vkDestroySurfaceKHR(m_instance, m_surface, nullptr);
m_surface = VK_NULL_HANDLE;
}
#if defined(VK_USE_PLATFORM_METAL_EXT)
if (m_platformLibrary != nullptr) {
Release(reinterpret_cast<id>(m_platformLibrary));
m_platformLibrary = nullptr;
}
if (m_platformDisplay != nullptr) {
Release(reinterpret_cast<id>(m_platformDisplay));
m_platformDisplay = nullptr;
}
#endif
#if defined(VK_USE_PLATFORM_XLIB_KHR)
if (m_platformDisplay != nullptr) {
// No fallback window to destroy any more: the display here is only ever
// one this renderer opened for a REAL window surface, and that window is
// the caller's to own. The hidden-window pbuffer fallback that used to be
// cleaned up here is gone (see CreateSurface).
using XCloseDisplayFn = int (*)(Display*);
auto* closeDisplay = reinterpret_cast<XCloseDisplayFn>(m_platformCloseDisplay);
if (closeDisplay) {
closeDisplay(static_cast<Display*>(m_platformDisplay));
}
m_platformDisplay = nullptr;
}
m_platformCloseDisplay = nullptr;
if (m_platformLibrary != nullptr) {
dlclose(m_platformLibrary);
m_platformLibrary = nullptr;
}
#endif
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
// The AImageReader owns the ANativeWindow the pbuffer fallback handed to the
// WSI, so it outlives the surface and is released only here.
if (m_fallbackImageReader != nullptr && m_platformLibrary != nullptr) {
using AImageReaderDeleteFn = void (*)(void*);
auto* imageReaderDelete =
reinterpret_cast<AImageReaderDeleteFn>(dlsym(m_platformLibrary, "AImageReader_delete"));
if (imageReaderDelete) {
imageReaderDelete(m_fallbackImageReader);
}
m_fallbackImageReader = nullptr;
m_window = 0;
dlclose(m_platformLibrary);
m_platformLibrary = nullptr;
}
#endif
if (m_debugMessenger != VK_NULL_HANDLE) {
DestroyDebugMessenger();
m_debugMessenger = VK_NULL_HANDLE;
}
DestroyDebugReportCallback();
if (m_instance != VK_NULL_HANDLE) {
vkDestroyInstance(m_instance, nullptr);
m_instance = VK_NULL_HANDLE;
}
MGLOG_I("VulkanRenderer shut down completed");
}
Bool VulkanRenderer::InitializeBlitResources() {
// P5f fv gave this an early return for every non-monolith transport, because the hidden
// programs are frontend objects and creating them on the server would retain a client
// compiler/allocator dependency. P7 wave 2-B2 (CONTRACT-P7 §5.2, (B')) takes the last
// step: in a disaggregated build there is NOTHING TO INITIALIZE on any transport,
// because the monolith arm now blits with WireColorBlit.cpp's baked module too.
//
// AN `#if` AND NOT A RUNTIME BRANCH, which is the whole point (§4.2): the early return
// left every ShaderObject / ProgramObject / SamplerObject symbol REFERENCED by this
// object file, so the link-closure ratchet counted them whether or not the branch ever
// ran. Compiling the construction out is what makes them fall.
return true;
}
void VulkanRenderer::ShutdownBlitResources() {
// P7 wave 2-B2: TWO OF P5e RULING 12'S FOUR APPLY-THREAD ALLOCATOR DEBTS ARE GONE, and
// the scope that named them with them. The debt was real when it was written: the hidden
// blit program and its samplers are FRONTEND objects, and under an active transport the
// server backend created them on the apply thread and destroyed them here on it, where
// their destructors run the client death helper and its lifetime-id probe.
//
// What retires it is not a new exemption but the absence of the objects. P5f fv gave
// InitializeBlitResources an early return for every non-monolith transport, so off the
// monolith arm `m_blitResources` never holds anything and this assignment destroys
// nothing - MEASURED, not argued: a temporary probe here and in
// ShutdownDepthMipmapResources failed the session if either resource was non-null under
// a non-monolith transport, and integration-magma-{split,spawn,tcp} (93/72/74),
// integration-magma-full-{split,spawn} (524/524), integration-split (187) and
// integration-magma-buffers (21) all stayed green with it armed. Dropping its transport
// test killed EVERY monolith `DirectVulkan.` entry at teardown and left the split
// entries green, which is what makes the first run evidence rather than an absence.
//
// The TYPE stays: MG_Test/Wire/RemoteClientTest's RemoteGuards
// .BarrieredLegacyScopesCannotExemptAllocator and
// .BarrieredFrontendRegistryMembersRefuseBothLegacyScopes use it as a NEGATIVE CONTROL -
// they construct it on the apply thread and assert the allocator still refuses, which is
// the thing P5f (fr) changed and the only remaining reason for it to exist.
m_blitResources = {};
}
Bool VulkanRenderer::InitializeDepthMipmapResources() {
// P7 wave 2-B2 (CONTRACT-P7 §5.2, (B')): the depth-mip half of the same step. In a
// disaggregated build the monolith arm generates its depth chain with
// WireDepthMipmap.cpp's baked pass, so there is no hidden GL program to build on any
// transport. See InitializeBlitResources above for why this is an `#if`.
return true;
}
void VulkanRenderer::ShutdownDepthMipmapResources() {
// P7 wave 2-B2: the second of the pair, retired for ShutdownBlitResources's reason and
// proved by the same probe. InitializeDepthMipmapResources carries the same non-monolith
// early return, so off the monolith arm this assignment destroys nothing.
m_depthMipmapResources = {};
}
void VulkanRenderer::CollectDeferredDepthMipmapCleanup(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_deferredDepthMipmapCleanup.size(),
"CollectDeferredDepthMipmapCleanup: frame index %u out of range (size=%zu)",
frameIndex, m_deferredDepthMipmapCleanup.size());
if (m_device == VK_NULL_HANDLE) {
return;
}
auto& cleanup = m_deferredDepthMipmapCleanup[frameIndex];
for (auto framebuffer : cleanup.framebuffers) {
if (framebuffer != VK_NULL_HANDLE) {
vkDestroyFramebuffer(m_device, framebuffer, nullptr);
}
}
for (auto pipeline : cleanup.pipelines) {
if (pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, pipeline, nullptr);
}
}
for (auto renderPass : cleanup.renderPasses) {
if (renderPass != VK_NULL_HANDLE) {
vkDestroyRenderPass(m_device, renderPass, nullptr);
}
}
for (auto imageView : cleanup.imageViews) {
if (imageView != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, imageView, nullptr);
}
}
cleanup.framebuffers.clear();
cleanup.pipelines.clear();
cleanup.renderPasses.clear();
cleanup.imageViews.clear();
}
void VulkanRenderer::DestroyDeferredDepthMipmapCleanup() {
for (Uint32 frameIndex = 0; frameIndex < m_deferredDepthMipmapCleanup.size(); ++frameIndex) {
CollectDeferredDepthMipmapCleanup(frameIndex);
}
m_deferredDepthMipmapCleanup.clear();
}
VkInstance VulkanRenderer::GetInstance() const {
return m_instance;
}
Bool VulkanRenderer::GetWireAhbImport(WireAhbImport& out) const {
out = WireAhbImport{};
if (!m_wireAhbImport || m_device == VK_NULL_HANDLE || m_wireGetAhbProperties == nullptr) return false;
out.device = m_device;
out.queue = m_graphicsQueue;
out.queueFamily = static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily);
vkGetPhysicalDeviceMemoryProperties(m_physicalDevice.handle, &out.memory);
out.getAhbProperties = m_wireGetAhbProperties;
return true;
}
} // namespace MobileGL::MG_Backend::DirectVulkan