mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-14 13:33:23 +09:00
1733 lines
112 KiB
C++
1733 lines
112 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h
|
|
// 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
|
|
|
|
#pragma once
|
|
#include "Config.h"
|
|
#include "FrameContext.h"
|
|
#include "MagmaPipeArms.h"
|
|
#include "PipelineFactory.h"
|
|
#include "ProgramFactory.h"
|
|
#include "SwapchainObject.h"
|
|
#include "UniformManager.h"
|
|
#include "VertexInputStateFactory.h"
|
|
#include "VkBufferObject.h"
|
|
#include "VkBufferManager.h"
|
|
#include "VkClearManager.h"
|
|
#include "VkRenderPassManager.h"
|
|
#include "VkSamplerManager.h"
|
|
#include "VkTextureManager.h"
|
|
#include "VkTimerQueryManager.h"
|
|
#include "GpuProgressMarkers.h"
|
|
#include "WireRenderPassCompatibility.h"
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
#include <MG_Remote/Server/SharedImageRegistry.h>
|
|
#endif
|
|
#include "MG_Util/Math/VectorTypes.h"
|
|
#include <Includes.h>
|
|
#include <MG_Backend/BackendObject.h>
|
|
#include <MG_Pipe/MGPipeHandles.h>
|
|
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
|
// The applier's CSO store: MGPipeApplier().BoundRenderStateCso is what the pipeline memo
|
|
// keys on after P2 (D12.1). Push-only, so the pull build's include graph is unchanged.
|
|
#include <MG_Pipe/PipeApply.h>
|
|
#include <vk_mem_alloc.h>
|
|
|
|
#include "../VkIncludes.h"
|
|
|
|
namespace MobileGL::MG_State::GLState {
|
|
class FramebufferObject;
|
|
class ProgramObject;
|
|
class SamplerObject;
|
|
class VertexArrayObject;
|
|
} // namespace MobileGL::MG_State::GLState
|
|
|
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
|
enum class DrawSetupAspect: Uint8 {
|
|
FramebufferObject = 1 << 0,
|
|
VertexArrayObject = 1 << 1,
|
|
UniformBuffer = 1 << 2,
|
|
VertexBuffer = 1 << 3,
|
|
IndexBuffer = 1 << 4,
|
|
IndirectDrawBuffer = 1 << 5,
|
|
Viewport = 1 << 6,
|
|
Scissor = 1 << 7,
|
|
};
|
|
|
|
struct DrawCmdParam {
|
|
Uint32 vertexCount = 0;
|
|
Uint32 instanceCount = 1;
|
|
Uint32 firstVertex = 0;
|
|
Uint32 firstInstance = 0;
|
|
// Indexed-draw metadata for bounding vertex-stream conversion. baseVertex is the
|
|
// draw's base-vertex offset; indexRangeIsExactView is true only when the draw
|
|
// fetches exactly the indices its IndexBufferView describes (direct DrawElements;
|
|
// multi/indirect forms leave it false because the CPU cannot bound their ranges).
|
|
Int32 baseVertex = 0;
|
|
Bool indexRangeIsExactView = false;
|
|
};
|
|
|
|
struct DrawIndexedCmdParam {
|
|
Uint32 indexCount = 0;
|
|
Uint32 instanceCount = 1;
|
|
Uint32 firstIndex = 0;
|
|
Int32 vertexOffset = 0;
|
|
Int32 firstInstance = 0;
|
|
};
|
|
|
|
struct DrawCmd {
|
|
GLenum mode = GL_TRIANGLES;
|
|
DrawCmdParam params;
|
|
};
|
|
|
|
struct IndexBufferView {
|
|
GLenum indexType = GL_UNSIGNED_SHORT;
|
|
SizeT indexByteOffset = 0;
|
|
SizeT indexByteSize = 0;
|
|
// Interpret indexByteOffset as a raw client pointer even when an element
|
|
// array buffer is bound (backend-synthesized index lists, e.g. the
|
|
// GL_LINE_LOOP -> LINE_STRIP rewrite).
|
|
Bool forceClientMemory = false;
|
|
};
|
|
|
|
struct DrawIndexedCmd {
|
|
GLenum mode = GL_TRIANGLES;
|
|
IndexBufferView indexBufferView;
|
|
|
|
DrawIndexedCmdParam params;
|
|
};
|
|
|
|
struct MultiDrawIndexedCmd {
|
|
GLenum mode = GL_TRIANGLES;
|
|
IndexBufferView indexBufferView;
|
|
|
|
Uint32 drawCount = 0;
|
|
DrawIndexedCmdParam* pParams = nullptr;
|
|
};
|
|
|
|
struct MultiDrawCmd {
|
|
GLenum mode = GL_TRIANGLES;
|
|
Uint32 drawCount = 0;
|
|
DrawCmdParam* pParams = nullptr;
|
|
};
|
|
|
|
struct QueueFamilyIndices {
|
|
Int32 graphicsFamily = -1;
|
|
Int32 presentFamily = -1;
|
|
};
|
|
|
|
struct PhysicalDevice {
|
|
QueueFamilyIndices queueFamilies;
|
|
VkPhysicalDeviceProperties properties;
|
|
VkPhysicalDevice handle = VK_NULL_HANDLE;
|
|
|
|
Bool IsComplete() const {
|
|
return handle != VK_NULL_HANDLE && queueFamilies.graphicsFamily != -1 && queueFamilies.presentFamily != -1;
|
|
}
|
|
};
|
|
|
|
// VK_KHR_fragment_shading_rate as this renderer enabled it, which is everything
|
|
// GL_EXT_fragment_shading_rate needs to know about the device. The three feature flags are the
|
|
// ENABLED ones; the rest are the device's properties and are meaningful only under them.
|
|
struct FragmentShadingRateSupport {
|
|
Bool Pipeline = false;
|
|
Bool Primitive = false;
|
|
Bool Attachment = false;
|
|
Bool NonTrivialCombiners = false;
|
|
Bool WithShaderDepthStencilWrites = false;
|
|
Bool WithSampleMask = false;
|
|
Bool PrimitiveWithMultipleViewports = false;
|
|
Bool LayeredAttachments = false;
|
|
// Per sample count 1, 2, 4 .. 64: bit (log2(w) << 2 | log2(h)) per rate the device lists
|
|
// for that count (vkGetPhysicalDeviceFragmentShadingRatesKHR).
|
|
Uint16 RateMasks[7] = {};
|
|
Uint32 MinAttachmentTexelWidth = 0;
|
|
Uint32 MinAttachmentTexelHeight = 0;
|
|
Uint32 MaxAttachmentTexelWidth = 0;
|
|
Uint32 MaxAttachmentTexelHeight = 0;
|
|
Uint32 MaxAttachmentTexelAspectRatio = 0;
|
|
};
|
|
|
|
class VulkanRenderer : public IBufferCopyCommandProvider,
|
|
public FrameContext::IRecordingObserver,
|
|
public ProgramFactory::IEvictionObserver {
|
|
public:
|
|
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
|
|
~VulkanRenderer();
|
|
|
|
void Initialize();
|
|
void Shutdown();
|
|
|
|
// IBufferCopyCommandProvider: recording command buffer, outside any
|
|
// render pass, for immediate staged buffer copies.
|
|
VkCommandBuffer AcquireBufferCopyCommandBuffer() override;
|
|
// EGL_BUFFER_AGE_EXT of the active target's default framebuffer for the next frame: the age
|
|
// of the swapchain image acquired for it (SwapchainObject::BufferAgeOf). Asking makes the
|
|
// target keep its presented images' content from then on.
|
|
Int32 CurrentDrawBufferAge();
|
|
VkBufferManager& GetWireBufferManager() { return m_bufferManager; }
|
|
Bool FlushWirePendingCommandsForTextureUpdate() {
|
|
// A new texture upload must not race graphics work that samples
|
|
// the old texels. No current recording does not imply no in-flight
|
|
// submission: finish the recorded work, then wait its whole prefix.
|
|
if (HasPendingRecordedWork() && !FlushPendingCommands()) return false;
|
|
return WaitForSubmitsUpTo(m_submitCounter, UINT64_MAX);
|
|
}
|
|
// The level-upload arm's half of the above, WITHOUT the CPU wait. Its bytes go through a
|
|
// staging buffer into an upload batch that is submitted on this same queue after this
|
|
// flush, and the batch's first barrier takes its source scope from the image's tracked
|
|
// layout (ALL_COMMANDS for GENERAL, the sampled-read stages for a read-only layout), so
|
|
// queue submission order already puts every earlier draw's access to the old texels
|
|
// ahead of the copy. The preserve arm keeps the wait: it retires the old image.
|
|
// (rd12 Magma: the wait was ~1.8 ms/frame of fence stall, one per lightmap update.)
|
|
// P15 (WireFramebuffer.inc): runs an image's deferred clears now, outside any pass.
|
|
void MaterializeWirePendingClears(VkTextureManager::TextureResource& resource);
|
|
Bool FlushWirePendingCommandsForTextureUpload() {
|
|
return !HasPendingRecordedWork() || FlushPendingCommands();
|
|
}
|
|
// P7 wave 2 package B3: the submission that will carry whatever is recorded NEXT.
|
|
// RetireWireObjects (WireDraw.inc) already tags future objects with exactly this, and
|
|
// for the same reason: a draw being set up now is not in any submission yet, so the
|
|
// index that names it is one past the counter. Deliberately NOT
|
|
// GetSyncPointSubmitIndex(), which answers m_submitCounter when nothing is recorded -
|
|
// true for a fence taken at that instant, wrong for work about to be recorded.
|
|
Uint64 GetWireNextSubmitIndex() const { return m_submitCounter + 1; }
|
|
// P8-D: the wire arm's indirect draw families, issued as vkCmdDraw[Indexed]Indirect[Count]
|
|
// from the wire stores MGPipeApplier().VerbIndirectBuffer / VerbIndirectParameterBuffer
|
|
// name. `offset` / `countOffset` are byte offsets into those stores; `stride` 0 means
|
|
// tightly packed. FALSE = this device cannot issue the COUNT form natively (the monolith
|
|
// arm's own condition, see the definition in WireDraw.inc) and the caller must read the
|
|
// words on the CPU; TRUE = issued, or declined by name.
|
|
Bool DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount, GLsizei stride,
|
|
Bool indexed, Bool counted, Uint64 countOffset);
|
|
// P11 B2 (T0): what importing a client's AHardwareBuffer as a wire buffer store needs from
|
|
// this device. False when the device did not take VK_ANDROID_external_memory_android_
|
|
// hardware_buffer at creation (not Android, not advertised, or a monolith device).
|
|
struct WireAhbImport {
|
|
VkDevice device = VK_NULL_HANDLE;
|
|
VkQueue queue = VK_NULL_HANDLE;
|
|
Uint32 queueFamily = 0;
|
|
VkPhysicalDeviceMemoryProperties memory{};
|
|
void* getAhbProperties = nullptr; // PFN_vkGetAndroidHardwareBufferPropertiesANDROID
|
|
};
|
|
Bool GetWireAhbImport(WireAhbImport& out) const;
|
|
// SHARED IMAGES (BackendObject::BlitDefaultFramebufferToSharedImage): the active surface
|
|
// target's default framebuffer, copied into `image` top row first and scaled to its
|
|
// extent. Returns once the copy is submitted, with its fence published to the image
|
|
// (SharedImages::PublishWrite); waits for it on the CPU only on a device without sync_file
|
|
// export. False (logged) when it cannot be done here: no AHardwareBuffer import, a
|
|
// quarter-turned surface, an unblittable format.
|
|
//
|
|
// `region` (GL window coordinates; Full = all) is the part to copy into an image this
|
|
// renderer has written before - the rest of it is left as it is; a first write, a scaled
|
|
// or turned copy copies all of it.
|
|
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image, const MG_Util::Damage::Region& region);
|
|
// IMPLICIT SYNC BY FLUSH (BackendObject::PublishSharedImageAccesses): every image acquired
|
|
// since the last boundary goes back to the foreign family, the recording is submitted with
|
|
// the exportable semaphore, and its sync_file is published to each as a write and a read.
|
|
// From then on an image's first acquire per frame also waits for its pending reads.
|
|
Bool PublishSharedImageAccesses();
|
|
// EGL_ANDROID_native_fence_sync's fence command (BackendObject::ExportNativeFence): the
|
|
// submission's sync_file in `*fence` (-1: none, and the work is waited out here), and - for
|
|
// a session that writes images - the boundary PublishSharedImageAccesses makes.
|
|
Bool ExportNativeFence(int* fence);
|
|
// The reading side's per-use hook (WireSharedImage.inc): every shared-image texture the
|
|
// wire draw or dispatch being set up samples (VkTextureManager::NoteSharedImageUse) is
|
|
// acquired from the foreign family before it, when it moved or is not held.
|
|
void AcquireNotedSharedImages() {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (m_textureManager && m_textureManager->HasNotedSharedImageUses()) AcquireNotedSharedImagesSlow();
|
|
#endif
|
|
}
|
|
// One shared-image texture, before a use: a new write generation, or an image released at
|
|
// the last frame boundary, is acquired here - an ownership barrier recorded ahead of the
|
|
// use, the write's fence a wait of the submission carrying it - and noted as read.
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
void AcquireSharedImage(VkTextureManager::TextureResource& resource);
|
|
#else
|
|
// P13 W5: no transport, no shared image (only a server allocates one), so a texture's
|
|
// sharedImageId is never set and there is nothing to acquire.
|
|
void AcquireSharedImage(VkTextureManager::TextureResource&) {}
|
|
#endif
|
|
|
|
// FrameContext::IRecordingObserver: prepares the frame's timer-query
|
|
// pool (harvest + reset) right after the frame command buffer begins
|
|
// recording, before any render pass.
|
|
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
|
|
|
|
// ProgramFactory::IEvictionObserver: an aged-out program entry was
|
|
// destroyed; evict its compute pipeline and graphics pipelines (same
|
|
// idleness guarantee - they are only bound through draws/dispatches that
|
|
// stamp the program entry) and purge the descriptor-set cache entries
|
|
// keyed by its now-recyclable VkDescriptorSetLayout handle.
|
|
void OnProgramEvicted(ProgramFactory::HashType programHash,
|
|
VkDescriptorSetLayout descriptorSetLayout) override;
|
|
|
|
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
|
const DrawCmdParam& drawParams,
|
|
const IndexBufferView* pIndexBufferView = nullptr);
|
|
enum class ScissoredClearPrep {
|
|
NotNeeded, // scissor covers the whole target — take the deferred whole-surface path instead
|
|
NoOp, // nothing to clear (degenerate target or empty scissor rect)
|
|
Ready, // a render pass is active; record vkCmdClearAttachments with the returned rect
|
|
};
|
|
void Clear(GLbitfield mask);
|
|
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
|
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
|
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
|
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
|
|
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
|
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
|
GLbitfield mask, GLenum filter);
|
|
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
|
GLint x, GLint y, GLsizei width, GLsizei height);
|
|
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
|
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
|
const CopyImageEndpoint& dstEndpoint,
|
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
|
void GenerateMipmap(GLenum target);
|
|
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
|
// Server resource handle in, tightly packed owned bytes out. No frontend
|
|
// texture, pixel-pack state or PBO is consulted by this readback.
|
|
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& ownedBytes);
|
|
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
|
// expects command recording to be active and any render pass already ended.
|
|
//
|
|
// `defaultFramebufferOrientation` is set only when the source is the swapchain's
|
|
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
|
|
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
|
|
// the way back, exactly as the colour ReadPixels path does.
|
|
// `sourceLayerCount` above 1 says the `height` rows the client is owed are stored as that
|
|
// many ARRAY LAYERS of a one-row image rather than as rows of one layer - the shape a GL
|
|
// 1D array has in Vulkan. The two produce byte-identical tightly-packed readbacks, so
|
|
// only the copy region differs; everything after it is written against `height`.
|
|
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
|
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
|
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
|
void* pixels, Bool defaultFramebufferOrientation = false,
|
|
Uint32 sourceLayerCount = 1);
|
|
// Same-extent depth blit between images of different depth formats: host
|
|
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
|
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
|
VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer,
|
|
VkImage dstImage, VkFormat dstFormat, VkImageLayout* dstTrackedLayout,
|
|
Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
|
|
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
|
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
|
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
|
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
|
|
// Quarter-turn surface transforms swap the copy extent's axes.
|
|
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
|
|
VkExtent2D imageExtent,
|
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
|
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
|
|
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
|
|
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
|
|
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
|
|
Uint32 logicalHeight,
|
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
|
SizeT texelSize, Uint8* outPixels);
|
|
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
|
GLsizei width, GLsizei height, GLenum destinationFormat,
|
|
GLenum destinationType, SizeT destinationRowStride,
|
|
Uint8* destinationPixels);
|
|
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
|
void DispatchComputeIndirect(GLintptr indirect);
|
|
void MemoryBarrier(GLbitfield barriers);
|
|
static VkMemoryBarrier BuildMemoryBarrierForGlBarriers(GLbitfield barriers);
|
|
void DrawArrays(const DrawCmd& payload);
|
|
void DrawElements(const DrawIndexedCmd& payload);
|
|
void MultiDrawArrays(const MultiDrawCmd& payload);
|
|
void MultiDrawElements(const MultiDrawIndexedCmd& payloads);
|
|
void Present();
|
|
|
|
const PhysicalDevice& GetPhysicalDevice() const;
|
|
VkInstance GetInstance() const;
|
|
// GL fence support, expressed in queue-submission indices backed by
|
|
// real VkFences. A GL fence captures GetSyncPointSubmitIndex() at
|
|
// creation: the index of the submission that will carry the commands
|
|
// recorded so far (m_submitCounter + 1 while work is pending, or
|
|
// m_submitCounter when nothing has been recorded since the last
|
|
// submit). It is signaled once that submission's fence is observed
|
|
// signaled - unlike the frame-serial heuristic, this makes fences
|
|
// signal as soon as the GPU actually finishes, which MC 1.21.5's
|
|
// fence-paced ring buffers rely on to recycle their space.
|
|
Uint64 GetSyncPointSubmitIndex() const;
|
|
// For tests: the per-frame descriptor pools (see DescriptorPoolCensus.h).
|
|
DescriptorPoolCensus GetDescriptorPoolCensus() const {
|
|
return m_uniformManager ? m_uniformManager->GetDescriptorPoolCensus() : DescriptorPoolCensus{};
|
|
}
|
|
// Non-blocking: polls outstanding submission fences and reports
|
|
// whether every submission up to `submitIndex` has completed.
|
|
Bool IsSubmitIndexComplete(Uint64 submitIndex);
|
|
// Submits the commands recorded so far without waiting (GL flush).
|
|
// Recording restarts lazily on a fresh command buffer; the submitted
|
|
// one is retired until the frame slot's fence is next waited. Returns
|
|
// true when a submission was made.
|
|
Bool FlushPendingCommands();
|
|
// DEVICE LOSS. A GPU fault or hang the driver reset leaves this renderer's VkDevice lost:
|
|
// every later submit and wait answers VK_ERROR_DEVICE_LOST. NoteDeviceLoss records it at
|
|
// the submit/wait sites (cheap, any result); IsDeviceLost answers from the record and,
|
|
// when nothing was recorded, asks the device once (only failure paths call it, so the
|
|
// probe's wait costs nothing on a healthy frame). Each session owns its own VkDevice, so a
|
|
// loss here is this session's alone.
|
|
void NoteDeviceLoss(VkResult result, const char* where);
|
|
Bool IsDeviceLost();
|
|
// For a wire site whose submit or wait just failed: true when the failure is a lost
|
|
// device, in which case this session has been latched (MGPipeSessionLatch - it ends, the
|
|
// display server's other sessions do not) and the caller must return from its verb
|
|
// without recording or submitting anything more. False: an ordinary failure, the site
|
|
// keeps the death it had. `site` names it in the latch line.
|
|
Bool LatchWireDeviceLoss(const char* site);
|
|
// THE GPU HANG WATCH (GpuProgressMarkers.h): true once the watch named one of this device's
|
|
// submissions as the one that stops the GPU. The device is then treated as lost, this session
|
|
// is latched, and the caller returns from its verb without submitting anything more. Asked
|
|
// before every frame submission and by the apply loop before it applies another record.
|
|
Bool LatchIfGpuHung(const char* site);
|
|
// Flush gated on usefulness: only flushes when `submitIndex` is still
|
|
// unsubmitted, so poll loops on already-submitted fences do not split
|
|
// the frame's render pass (a full tile load/store on TBDR GPUs).
|
|
Bool FlushForSyncPoint(Uint64 submitIndex);
|
|
// Blocking wait for a submission index with a nanosecond timeout.
|
|
// When the index is still unsubmitted and flushIfPending is set, the
|
|
// pending commands are flushed first so the wait can make progress.
|
|
Bool WaitForSubmitIndex(Uint64 submitIndex, Uint64 timeoutNs, Bool flushIfPending);
|
|
|
|
// Frame-serial completion, still used by the timer-query paths (their
|
|
// records are bucketed per frame slot).
|
|
Bool IsFrameSerialComplete(Uint64 serial) const;
|
|
// Blocking wait for a submitted serial. Returns false when the serial
|
|
// cannot complete without further submissions (it belongs to the
|
|
// current, not-yet-presented frame) or when the wait failed.
|
|
Bool WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs);
|
|
|
|
// GPU timer queries, backing the GL_TIME_ELAPSED / GL_TIMESTAMP
|
|
// frontend. Timestamp support (queue timestampValidBits > 0 and a
|
|
// non-zero timestampPeriod) is cached at device creation.
|
|
Bool IsTimerQuerySupported() const;
|
|
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
|
// honored rather than accepted-and-ignored.
|
|
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
|
const FragmentShadingRateSupport& GetFragmentShadingRateSupport() const { return m_fragmentShadingRate; }
|
|
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
|
|
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
|
|
// complete contract: one legalizes the command word, the other enables the shader rebase.
|
|
Bool IsNonZeroIndirectBaseInstanceSupported() const {
|
|
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
|
|
}
|
|
// Ensures the frame command buffer is recording (same lazy pattern as
|
|
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
|
// frame's pool. Null when unsupported or the pool is exhausted.
|
|
SharedPtr<VkTimerQueryManager::TimestampRecord> WriteTimerQueryTimestamp();
|
|
// Non-blocking: true once the record's raw ticks are on the CPU
|
|
// (harvests the slot once its frame serial has completed).
|
|
Bool IsTimerQueryResultReady(VkTimerQueryManager::TimestampRecord& record);
|
|
// Blocking wait, mirroring ClientWaitSync's caveat: a record written
|
|
// this frame cannot complete until Present submits the commands, so
|
|
// this returns false (result reads as 0) instead of deadlocking.
|
|
Bool WaitForTimerQueryResult(VkTimerQueryManager::TimestampRecord& record);
|
|
Uint64 GetTimerQueryElapsedNs(const VkTimerQueryManager::TimestampRecord& begin,
|
|
const VkTimerQueryManager::TimestampRecord& end) const;
|
|
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
|
|
|
|
// GL_SAMPLES_PASSED occlusion queries: every app draw between Start and Stop is
|
|
// wrapped in a Vulkan occlusion query slot; the result is the slot sum. Requires
|
|
// hostQueryReset for slot recycling - Start fails (frontend keeps the query
|
|
// unsupported) when the device lacks it.
|
|
Bool StartOcclusionQueryCapture();
|
|
void StopOcclusionQueryCapture(Vector<Uint32>& outSlots);
|
|
// Flushes pending commands, waits, sums the slots, and recycles them.
|
|
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
|
|
|
|
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
|
// eglSwapInterval. Records the request; Present rebuilds the swapchain for it once the
|
|
// current frame is on screen, and only when it maps to a different present mode. A
|
|
// renderer created without an app window ignores it: its surface (headless, or an
|
|
// AImageReader nobody consumes) is never displayed, and FIFO there would block once
|
|
// the reader's queue fills.
|
|
void SetSwapInterval(Int interval);
|
|
// Re-query the surface and report whether the live swapchain no longer matches it
|
|
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
|
|
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
|
|
Bool SwapchainIsOutOfDate();
|
|
// Returns false when the surface is zero-area (minimized/hidden window):
|
|
// no new swapchain is installed and presentation must stay suspended.
|
|
Bool RecreateSwapchain();
|
|
|
|
// SURFACE TARGETS. One renderer - one device and every GL object's resources - serves
|
|
// every EGL surface of its context's client; only the presentation side (VkSurfaceKHR,
|
|
// swapchain, its render-finished semaphores, the acquired image, the pbuffer's image
|
|
// reader) belongs to a surface. `key` names the client surface. The renderer starts with
|
|
// the surface it was constructed for as its active target (SetActiveSurfaceTargetKey).
|
|
// Activating another key parks the active target - its recorded work submitted, its
|
|
// pending acquire consumed - and brings the other back, or builds it on first use.
|
|
void SetActiveSurfaceTargetKey(Uint64 key) { m_activeTargetKey = key; }
|
|
// False when a NEW target's surface could not be built (logged); the renderer is then left
|
|
// with no active target - every present drops its frame - and the caller refuses by name.
|
|
Bool ActivateSurfaceTarget(Uint64 key, NativeWindowType window, const VulkanRendererConfig& surfaceConfig);
|
|
// Drops a surface's target, active or parked. A no-op for a key that has none.
|
|
void DestroySurfaceTarget(Uint64 key);
|
|
// The default framebuffer's extent (surface space, what PublishDefaultFramebufferInfo
|
|
// publishes) of `key`'s target, active or parked; 0x0 when it has none or no swapchain.
|
|
VkExtent2D SurfaceTargetExtent(Uint64 key) const;
|
|
// The active WINDOW target's window changed size. Between frames (nothing recorded since the last
|
|
// present) its swapchain is rebuilt now - which publishes the new extent - so a client that has
|
|
// nothing to redraw learns it without presenting; with a frame half-recorded the rebuild waits
|
|
// for that frame's present (it would otherwise land half in each swapchain). False: no active
|
|
// window target.
|
|
Bool FollowActiveWindowResize();
|
|
// Whether `key` names the active target.
|
|
Bool IsActiveSurfaceTarget(Uint64 key) const { return key == m_activeTargetKey && m_surface != VK_NULL_HANDLE; }
|
|
|
|
private:
|
|
// The presentation half of the renderer, parked while another surface is active. Its
|
|
// fields mirror the renderer members ExchangeActiveTarget swaps them with.
|
|
struct SurfaceTarget {
|
|
Uint64 serial = 0;
|
|
NativeWindowType window = 0;
|
|
void* platformDisplay = nullptr;
|
|
void* platformLibrary = nullptr;
|
|
void* platformCloseDisplay = nullptr;
|
|
void* fallbackImageReader = nullptr;
|
|
Uint32 surfaceWidth = 0;
|
|
Uint32 surfaceHeight = 0;
|
|
Optional<Int> swapInterval;
|
|
Bool presentsToAppWindow = false;
|
|
Optional<Int> swapchainSwapInterval;
|
|
Bool swapchainResizeRequested = false;
|
|
Bool presentSuspended = false;
|
|
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
|
SwapchainObject swapchain;
|
|
Vector<VkSemaphore> renderFinishedSemaphores;
|
|
Uint imageIndexAcquired = 0;
|
|
Uint defaultFramebufferImageIndex = 0;
|
|
};
|
|
void ExchangeActiveTarget(SurfaceTarget& target);
|
|
// Submits the active target's work, consumes its pending acquire and moves it into
|
|
// m_parkedTargets. Leaves the renderer with no target.
|
|
void ParkActiveTarget();
|
|
void OnActiveTargetChanged();
|
|
void DestroyParkedTarget(SurfaceTarget& target);
|
|
UnorderedMap<Uint64, SurfaceTarget> m_parkedTargets;
|
|
Uint64 m_activeTargetKey = 0;
|
|
Uint64 m_activeTargetSerial = 1;
|
|
Uint64 m_nextTargetSerial = 2;
|
|
|
|
// Tiered emission for an already-set-up multi-draw batch (state bound, index
|
|
// buffer bound for the indexed form). Tier 1: VK_EXT_multi_draw. Tier 2: one
|
|
// vkCmdDraw(Indexed)Indirect over a transient command array. Tier 3: unrolled
|
|
// vkCmdDraw(Indexed) loop. Tier eligibility is per-batch (uniform instance
|
|
// state for tier 1, firstInstance/feature legality for tier 2); every tier
|
|
// consumes the same param span, so contiguous-run merging done by the caller
|
|
// benefits all of them.
|
|
void EmitMultiDrawIndexed(VkCommandBuffer commandBuffer, const DrawIndexedCmdParam* pParams, Uint32 drawCount);
|
|
void EmitMultiDraw(VkCommandBuffer commandBuffer, const DrawCmdParam* pParams, Uint32 drawCount);
|
|
|
|
struct BlitUniformData {
|
|
float srcRect[4] = {0.f, 0.f, 1.f, 1.f};
|
|
float dstRect[4] = {0.f, 0.f, 1.f, 1.f};
|
|
Int surfaceTransform = 0;
|
|
Int padding[3] = {0, 0, 0};
|
|
};
|
|
|
|
// P7 wave 2-B2, CONTRACT-P7 §5.2 (B'): THE FRONTEND MEMBERS ARE PULL-BUILD ONLY. In a
|
|
// disaggregated build the monolith arm blits and generates depth mips with the baked
|
|
// modules (WireColorBlit.inc, WireDepthMipmap.inc), so no DirectVulkan object holds a
|
|
// ProgramObject, a ShaderObject or a SamplerObject any more - which is what takes
|
|
// ProgramObject::{AllocateLifetimeId, AttachShader, Link, ~ProgramObject},
|
|
// ShaderObject::{SetShaderSource, Compile, JoinPendingCompile, ReleaseCompileNode,
|
|
// DropCompileNode} and SamplerObject::{SetWrapS, SetWrapT, SetWrapR, SetLodRange} out
|
|
// of the link ratchet's `p7-magma` bucket (§4.2). The destructor of a SharedPtr member
|
|
// is a reference too, so the MEMBER has to go and not only its construction.
|
|
//
|
|
// The structs stay, empty, so ShutdownBlitResources and the two `= {}` assignments read
|
|
// the same on both builds.
|
|
struct BlitResources {
|
|
};
|
|
|
|
struct DepthMipmapResources {
|
|
};
|
|
|
|
// A single-sample staging image for multisample-resolve blits that also have to change
|
|
// orientation. vkCmdResolveImage cannot flip (it takes one offset per side, not the
|
|
// invertible pair vkCmdBlitImage takes), so a resolve into or out of the default
|
|
// framebuffer used to land the mirrored band. Resolving here first and then blitting from
|
|
// here separates the two operations, and each one then does only what it can express.
|
|
//
|
|
// Pooled rather than created per blit: the CTS runs hundreds of these back to back, and
|
|
// create-destroy per call would both cost allocations and, worse, need per-call deferred
|
|
// destruction to outlive the recording. It grows to the largest extent asked for and is
|
|
// reused; format changes recreate it.
|
|
struct MultisampleResolveScratchImage {
|
|
VkImage image = VK_NULL_HANDLE;
|
|
VmaAllocation allocation = VK_NULL_HANDLE;
|
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
VkExtent2D extent = {0, 0};
|
|
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
};
|
|
MultisampleResolveScratchImage m_msResolveScratch;
|
|
void DestroyMultisampleResolveScratchImage();
|
|
|
|
struct DeferredDepthMipmapCleanup {
|
|
Vector<VkImageView> imageViews;
|
|
Vector<VkFramebuffer> framebuffers;
|
|
Vector<VkRenderPass> renderPasses;
|
|
Vector<VkPipeline> pipelines;
|
|
};
|
|
|
|
void QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload);
|
|
// See NoteDeviceLoss. Set once, never cleared: a lost VkDevice stays lost.
|
|
Bool m_deviceLost = false;
|
|
Bool m_deviceLossLatched = false;
|
|
// Brackets this device's frame submissions for the GPU hang watch (LatchIfGpuHung).
|
|
GpuProgressMarkers m_progressMarkers;
|
|
struct WireImage {
|
|
VkImage image = VK_NULL_HANDLE;
|
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
VkExtent2D extent{};
|
|
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
VkImageLayout* trackedLayout = nullptr;
|
|
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
|
VkImageAspectFlags aspect = 0;
|
|
// Zero for the default framebuffer, which no arm that reads this ever reaches.
|
|
VkImageUsageFlags usageFlags = 0;
|
|
Uint32 level = 0, layer = 0, layers = 1, levels = 1;
|
|
Bool isDefault = false;
|
|
// Set when isDefault: the swapchain image index this role resolved to.
|
|
Uint32 swapchainImageIndex = 0;
|
|
Bool is3D = false;
|
|
MG_Pipe::MGPipeHandle storage = MG_Pipe::kMGPipeNullHandle;
|
|
// The storage's resource, for a non-default surface (null for the default framebuffer).
|
|
VkTextureManager::TextureResource* resource = nullptr;
|
|
};
|
|
// `isWriteTarget` selects which side of the default framebuffer this resolves: a write
|
|
// re-points it at the image Present() acquired, a read keeps the current one (see
|
|
// m_defaultFramebufferImageIndex). Reads must pass false.
|
|
WireImage ResolveWireImage(const MG_Pipe::MGPFramebufferState& fbo,
|
|
const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect,
|
|
Bool isWriteTarget = false);
|
|
void TransitionWireImage(WireImage& image, VkImageLayout layout, Bool coalesceWithLastFullBarrier = false);
|
|
// P15: WIRE DEFERRED CLEARS (WireFramebuffer.inc). A clear of images the open pass
|
|
// holds is recorded inside it; a whole-image clear of others waits on the image
|
|
// (WirePendingClear) for the next pass that draws into it (LOAD_OP_CLEAR) or the first
|
|
// other use (MaterializeWirePendingClears).
|
|
GLbitfield ClearWireAttachmentsOfOpenPass(const MG_Pipe::MGPFramebufferState& fbo,
|
|
const ClearAttachmentPayload& payload, GLint drawbuffer);
|
|
GLbitfield DeferWireClear(const MG_Pipe::MGPFramebufferState& fbo, const ClearAttachmentPayload& payload,
|
|
GLint drawbuffer);
|
|
// An attachment of the pass being prepared whose image carries pending clears; taken
|
|
// when the pass begins (BeginWireDrawPassFolding) or applied inside it if it is open.
|
|
struct WireFoldClear {
|
|
Uint32 attachment = 0;
|
|
VkImageAspectFlags aspects = 0;
|
|
VkTextureManager::TextureResource* resource = nullptr;
|
|
Uint32 level = 0, layer = 0, layers = 1;
|
|
};
|
|
Vector<WireFoldClear> m_wireFoldClears;
|
|
static Bool WireImageCanDeferClear(const WireImage& image);
|
|
static void NoteWirePendingClear(const WireImage& image, VkImageAspectFlags aspects, const VkClearValue& value);
|
|
VkRenderPass WireClearVariant(RenderPassEntry& begun, Uint64 variantKey);
|
|
Bool BeginWireDrawPassFolding(FrameContext::FrameData& frame, RenderPassEntry& begun, Uint64 variantKey,
|
|
const VkClearValue* values);
|
|
void ApplyWireFoldClearsInPass(FrameContext::FrameData& frame, RenderPassEntry& pass);
|
|
void ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
|
|
const ClearAttachmentPayload& payload, GLint drawbuffer = -1);
|
|
void BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
|
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter);
|
|
void ResolveWireDepthStencil(WireImage source, WireImage destination,
|
|
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
|
GLint dx0, GLint dy0, GLint dx1, GLint dy1);
|
|
// One aspect of a depth/stencil image through a buffer (source in TRANSFER_SRC_OPTIMAL,
|
|
// destination in TRANSFER_DST_OPTIMAL, one format). The returned buffer is named by the
|
|
// recorded commands and must be retired with them.
|
|
UniquePtr<VkBufferObject> CopyWireAspectThroughBuffer(VkImage srcImage, Uint32 srcLevel, Uint32 srcLayer,
|
|
GLint sx, GLint sy, const WireImage& destination,
|
|
GLint dx, GLint dy, Uint32 width, Uint32 height,
|
|
VkImageAspectFlags aspect, VkFormat format,
|
|
Bool mirrorY, const char* fatalDetail);
|
|
// A single-sample depth/stencil blit whose rectangles make it a plain copy, performed
|
|
// without vkCmdBlitImage (P7 gate 5). False when the shape is not a plain copy.
|
|
Bool CopyWireDepthStencilBlit(WireImage& source, WireImage& destination,
|
|
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
|
GLint dx0, GLint dy0, GLint dx1, GLint dy1, VkImageAspectFlags aspect);
|
|
// True when the rectangles give a multisample depth/stencil resolve a shape it declines
|
|
// (a scale, an X mirror, a Y mirror across two formats); the decline has then been
|
|
// logged and its INVALID_OPERATION recorded. BlitWireFramebuffers asks it BEFORE any
|
|
// aspect runs, so a call the SHAPE declines leaves every attachment alone - the user
|
|
// and the default draw framebuffer alike. The CAPABILITY decline inside
|
|
// ResolveWireDepthStencil (a stencil-only resolve without VK_EXT_shader_stencil_export
|
|
// on a device without VK_KHR_depth_stencil_resolve) is still decided per aspect, after
|
|
// colour ran; rule I (a) accepts that (notes/p7/magma-b2.md §2.10).
|
|
Bool DeclineWireDepthStencilResolveShape(const WireImage& source, const WireImage& destination,
|
|
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
|
GLint dx0, GLint dy0, GLint dx1, GLint dy1);
|
|
PFN_vkCreateRenderPass2 m_wireCreateRenderPass2 = nullptr;
|
|
VkResolveModeFlags m_wireDepthResolveModes = 0;
|
|
VkResolveModeFlags m_wireStencilResolveModes = 0;
|
|
// P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-aspect`): the SECOND arm of the
|
|
// multisample depth/stencil resolve, for the device that does not carry
|
|
// VK_KHR_depth_stencil_resolve. One baked pass per aspect, sample zero, no frontend
|
|
// object - see WireMultisampleResolve.inc. `resolved` must be a single-sample image of
|
|
// the source's format and extent; the rect is expressed by the scissor.
|
|
Bool ResolveWireDepthStencilWithShader(WireImage source, WireImage resolved,
|
|
VkImageAspectFlags aspect, GLint sx0, GLint sy0,
|
|
Uint32 width, Uint32 height);
|
|
struct WireMultisampleResolveResources;
|
|
WireMultisampleResolveResources* m_wireMultisampleResolveResources = nullptr;
|
|
void DestroyWireMultisampleResolveResources();
|
|
Bool m_wireShaderStencilExport = false;
|
|
// VK_EXT_robustness2 (or its KHR promotion) with ONLY nullDescriptor enabled, wire arms
|
|
// only: an invalid image unit binds a null storage descriptor (codex closeout finding 2).
|
|
Bool m_wireNullDescriptor = false;
|
|
// The resolve probe's verdict for this device (WireDepthResolveArm.h,
|
|
// WireDepthResolveProbe.h): true when the no-draw render pass was measured leaving its
|
|
// target unwritten while the shader control resolved - the shader arm then resolves
|
|
// first and the render-pass arm is the fallback (P7 gate 5, g5-msprobe).
|
|
Bool m_wirePreferShaderDepthResolve = false;
|
|
// P11 B2 (T0): the device took the AHardwareBuffer import extensions at creation, and the
|
|
// one entry point the import needs (PFN_vkGetAndroidHardwareBufferPropertiesANDROID).
|
|
Bool m_wireAhbImport = false;
|
|
void* m_wireGetAhbProperties = nullptr;
|
|
// YUV shared images (WireYuvImage.inc): the device enabled samplerYcbcrConversion, and the
|
|
// conversion pipelines made so far (one per buffer format and colour conversion).
|
|
Bool m_samplerYcbcrConversion = false;
|
|
struct WireYuvResources;
|
|
WireYuvResources* m_wireYuvResources = nullptr;
|
|
void DestroyWireYuvResources();
|
|
// A YUV texture's first use in a frame converts its image (yuvConvertedFrame against this,
|
|
// which every frame boundary - present, publishing flush, native fence - moves).
|
|
Uint64 m_yuvFrameSerial = 1;
|
|
Bool ConvertYuvSharedImage(VkTextureManager::TextureResource& resource);
|
|
// Shared images this session presents into, imported once each and keyed by the image's
|
|
// id (monotonic, never reused). `owner` watches the registry's image: an entry whose image
|
|
// died is dropped at a later present once `lastSubmit`, its last copy, has completed.
|
|
struct SharedImagePresentTarget {
|
|
VkTextureManager::ImportedSharedImage image;
|
|
WeakPtr<const void> owner;
|
|
Uint64 lastSubmit = 0;
|
|
// A copy has written the image through this import: what it holds is a frame.
|
|
Bool written = false;
|
|
};
|
|
UnorderedMap<Uint64, SharedImagePresentTarget> m_sharedImagePresentTargets;
|
|
void DestroySharedImagePresentTargets(Bool onlyDead);
|
|
|
|
// SHARED-IMAGE SYNCHRONISATION (WireSharedImage.inc). The sync_file fences other sessions
|
|
// publish become binary semaphores: a temporary SYNC_FD import is a WAIT, an exportable
|
|
// semaphore a SIGNAL, of the next frame submission - whichever of SubmitPendingCommandBuffer
|
|
// and Present's submit comes first, which is the one carrying what was recorded since.
|
|
// Each semaphore is recycled once the submission that used it has completed.
|
|
// m_sharedImageSyncFd: the device imports and exports SYNC_FD binary semaphores; without
|
|
// it the waits happen on the CPU and nothing is exported (see WireSharedImage.inc).
|
|
Bool m_sharedImageSyncFd = false;
|
|
// The session writes images - it ends frames at publishing flushes
|
|
// (PublishSharedImageAccesses), or it reached one through a framebuffer (ResolveWireImage):
|
|
// a first acquire waits for the image's pending reads as well, and its boundaries publish
|
|
// what it used as written.
|
|
Bool m_sharedImageImplicitSync = false;
|
|
PFN_vkImportSemaphoreFdKHR m_importSemaphoreFd = nullptr;
|
|
PFN_vkGetSemaphoreFdKHR m_getSemaphoreFd = nullptr;
|
|
struct SharedImageWait {
|
|
VkSemaphore semaphore = VK_NULL_HANDLE;
|
|
VkPipelineStageFlags stages = 0;
|
|
};
|
|
Vector<SharedImageWait> m_sharedImageWaits; // staged for the next submission
|
|
VkSemaphore m_sharedImageSignal = VK_NULL_HANDLE; // staged for the next submission
|
|
VkSemaphore m_sharedImageSignalSubmitted = VK_NULL_HANDLE; // the last one submitted, to export
|
|
struct SharedImageSemaphoreInFlight {
|
|
VkSemaphore semaphore = VK_NULL_HANDLE;
|
|
Uint64 submitIndex = 0;
|
|
Bool exportable = false;
|
|
Bool exported = false; // a signal whose sync_file was taken (which unsignals it)
|
|
};
|
|
Vector<SharedImageSemaphoreInFlight> m_sharedImageSemaphoresInFlight; // submit order
|
|
Vector<VkSemaphore> m_freeSharedImageWaitSemaphores;
|
|
Vector<VkSemaphore> m_freeSharedImageSignalSemaphores;
|
|
// The reading side, per session: the shared-image textures this frame holds (acquired, not
|
|
// yet released; keyed as in VkTextureManager's handle map, with the submission that carries
|
|
// the acquire), and the frame's ReadTracker, published at Present.
|
|
struct SharedImageHold {
|
|
Uint64 key = 0;
|
|
Uint64 acquireSubmit = 0;
|
|
};
|
|
Vector<SharedImageHold> m_sharedImagesHeld;
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
MG_Remote::Server::SharedImages::ReadTracker m_sharedImageReads;
|
|
#endif
|
|
Vector<Uint64> m_sharedImageUseScratch;
|
|
// What one vkQueueSubmit carries beyond its own semaphores (AttachSharedImageSync).
|
|
struct SharedImageSubmitSync {
|
|
Bool attached = false;
|
|
Vector<VkSemaphore> waits;
|
|
Vector<VkPipelineStageFlags> stages;
|
|
Vector<VkSemaphore> signals;
|
|
};
|
|
void AttachSharedImageSync(VkSubmitInfo& info, SharedImageSubmitSync& storage);
|
|
// After the submission `storage` was attached to was submitted and registered.
|
|
void CommitSharedImageSync(const SharedImageSubmitSync& storage);
|
|
// Takes ownership of `fence` (a sync_file; -1 = nothing to wait for).
|
|
void StageSharedImageWait(int fence, VkPipelineStageFlags stages);
|
|
VkSemaphore TakeSharedImageSemaphore(Bool exportable);
|
|
// The submitted signal's sync_file (-1: it has already signaled). False: not exported.
|
|
Bool ExportSharedImageSignal(VkSemaphore semaphore, int& fence);
|
|
void RecycleSharedImageSemaphores(Uint64 completedSubmit);
|
|
void DestroySharedImageSync();
|
|
void AcquireNotedSharedImagesSlow();
|
|
// The reading side's frame boundary: release every held image back to the foreign family
|
|
// in the open recording, and - after the submission carrying it - publish its fence.
|
|
void ReleaseHeldSharedImages();
|
|
void PublishSharedImageReads();
|
|
// A frame dropped unsubmitted (Present with no usable swapchain).
|
|
void AbandonSharedImageReads();
|
|
// Runs the probe (memoized per device identity) and sets the member above. Called at the end of
|
|
// device creation, after ArmPrimGenReroute: it records on m_graphicsQueue.
|
|
void ArmWireDepthResolveOrder();
|
|
Bool BlitWireColorToDefault(WireImage source, WireImage destination,
|
|
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
|
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter);
|
|
Bool BlitWireColorImage(WireImage source, WireImage destination,
|
|
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
|
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter, Bool mipmap);
|
|
// P7 wave 2-B, CONTRACT-P7 §3.2 (`default-color-blit-shape`): the rotated arm. Moves the
|
|
// source region into an owned single-sample 2D image of a sampled-float format in the
|
|
// same size-compatibility class, then shader-blits THAT to the rotated default
|
|
// framebuffer - the shapes BlitWireColorImage itself cannot sample (an integer format, a
|
|
// 3D or array source) become shapes it can.
|
|
Bool BlitWireColorToDefaultThroughScratch(WireImage source, WireImage destination,
|
|
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
|
GLint dx0, GLint dy0, GLint dx1, GLint dy1,
|
|
GLenum filter);
|
|
struct WireColorBlitResources;
|
|
WireColorBlitResources* m_wireColorBlitResources = nullptr;
|
|
void DestroyWireColorBlitResources();
|
|
void ResetWireColorBlitFramePool(Uint32 frameIndex);
|
|
void RetireWireColorBlitTargets(Bool all);
|
|
// P7 wave 2-B, CONTRACT-P7 §5.1 (A): the baked depth-mip program (WireDepthMipmap.inc).
|
|
// One level of a depth chain, source and destination being two depth subresources of
|
|
// the same image; the caller loops. It owns no frontend object, which is what the
|
|
// monolith arm's GenerateDepthMipmapWithShader cannot say.
|
|
Bool GenerateWireDepthMipLevel(WireImage source, WireImage destination);
|
|
struct WireDepthMipmapResources;
|
|
WireDepthMipmapResources* m_wireDepthMipmapResources = nullptr;
|
|
void DestroyWireDepthMipmapResources();
|
|
// P7 wave 2-B (exit gate 3): the readback's own write-visibility barrier, recorded before
|
|
// the copy instead of inferred from the image's tracked layout. See WireFramebuffer.inc.
|
|
void RecordWireReadbackWriteBarrier();
|
|
void ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height,
|
|
GLenum format, GLenum type, void* pixels);
|
|
void GenerateWireMipmap();
|
|
Bool SetupWireDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
|
const DrawCmdParam& drawParams, const IndexBufferView* indices);
|
|
void DestroyWireDrawPass();
|
|
void RetireWireDrawPass();
|
|
struct WireDrawAttachmentKey {
|
|
MG_Pipe::MGPipeHandle storage = MG_Pipe::kMGPipeNullHandle;
|
|
Uint32 resourceKind = 0;
|
|
VkImage image = VK_NULL_HANDLE;
|
|
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
VkImageAspectFlags aspect = 0, requestedAspect = 0;
|
|
Uint32 level = 0, layer = 0, layers = 1, imageLevels = 1;
|
|
Uint32 width = 0, height = 0;
|
|
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
|
Bool operator==(const WireDrawAttachmentKey&) const = default;
|
|
};
|
|
struct WireDrawPassKey {
|
|
Vector<WireDrawAttachmentKey> attachments;
|
|
Vector<Uint32> colorReferences;
|
|
Uint32 depthReference = VK_ATTACHMENT_UNUSED;
|
|
Uint32 width = 0, height = 0, layers = 1;
|
|
Uint64 textureImageEpoch = 0, resourceEraseEpoch = 0;
|
|
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
|
|
Uint32 swapchainImageIndex = 0;
|
|
Bool isDefault = false, framebufferSrgb = false;
|
|
Bool operator==(const WireDrawPassKey&) const = default;
|
|
// Default values, vectors emptied but their storage kept (the per-draw scratch).
|
|
void ResetKeepingCapacity() {
|
|
auto keptAttachments = Move(attachments);
|
|
auto keptColorReferences = Move(colorReferences);
|
|
*this = WireDrawPassKey{};
|
|
keptAttachments.clear();
|
|
keptColorReferences.clear();
|
|
attachments = Move(keptAttachments);
|
|
colorReferences = Move(keptColorReferences);
|
|
}
|
|
};
|
|
struct WireDrawPassCacheEntry {
|
|
WireDrawPassKey key;
|
|
UniquePtr<RenderPassEntry> pass;
|
|
Vector<VkImageView> views;
|
|
// The last submission that recorded this pass (P15 S0): it may be destroyed only once
|
|
// that submission completed.
|
|
Uint64 lastUseSubmit = 0;
|
|
};
|
|
// Objects only: every draw still ends/begins its pass and records the
|
|
// existing memory dependencies. P15 S0: ONE cache across frame slots, so a framebuffer
|
|
// drawn every frame keeps its render pass, framebuffer and views instead of rebuilding them
|
|
// each frame. An entry whose key's image epochs moved can never hit again (the key holds
|
|
// them) and is destroyed once its last use completed (ClearWireDrawPassCache).
|
|
Bool AcquireCachedWireDrawPass(const WireDrawPassKey& key);
|
|
void ClearWireDrawPassCache(Uint32 frameIndex);
|
|
void ClearAllWireDrawPassCaches();
|
|
void DestroyWireDrawPassCacheEntry(WireDrawPassCacheEntry& entry);
|
|
Vector<WireDrawPassCacheEntry> m_wireDrawPassCache;
|
|
WireDrawPassKey m_wireDrawPassKey;
|
|
// P15: the open pass's attachment that is `image` (WireFramebuffer.inc's deferred clears).
|
|
static Uint32 OpenPassAttachmentOf(const WireImage& image, const Vector<WireDrawAttachmentKey>& attachments);
|
|
WireRenderPassCompatibilityTable m_wireRenderPassCompatibility;
|
|
// The interned compatibility id of m_wireDrawPass's attachments, or 0 when not known yet.
|
|
// A draw that continues the pass (its WireDrawPassKey compares equal, which covers every
|
|
// input of the compatibility key) reuses it instead of building and hashing the key again.
|
|
Uint64 m_wireDrawPassCompatibilityId = 0;
|
|
// P14: SetupWireDraw's per-draw containers, cleared and refilled every draw instead of
|
|
// allocated and freed (the heap was ~10% of a Minecraft frame on the wire arm). A nested
|
|
// SetupWireDraw - nothing does that today - would use a local set (WireDraw.inc).
|
|
struct WireDrawScratch {
|
|
VertexInputStateFactory::BackendVertexInputState input;
|
|
Vector<BufferSlice> vertexSlices;
|
|
Vector<VkAttachmentDescription> attachments;
|
|
Vector<VkImageViewCreateInfo> viewInfos;
|
|
Vector<VkAttachmentReference> colors;
|
|
Vector<WireImage> sameLayoutAttachments;
|
|
WireDrawPassKey passKey;
|
|
WireRenderPassCompatibilityKey compatibilityKey;
|
|
struct GpuWriteMark {
|
|
MG_Pipe::MGPipeHandle storage;
|
|
Uint32 level, layer, layers;
|
|
};
|
|
Vector<GpuWriteMark> gpuWriteMarks;
|
|
};
|
|
// The current wire pass's attachments were marked GPU-written at this StagedTextureStore
|
|
// clear generation (SetupWireDraw); false whenever the pass changes.
|
|
Bool m_wirePassMarksValid = false;
|
|
// P14: THE OPEN PASS'S ATTACHMENTS, as the last full attachment walk resolved them. A draw
|
|
// into the same framebuffer record (FramebufferSerial moves on every framebuffer write,
|
|
// ContextSerial on every applier reset) while the pass is still open on this command buffer,
|
|
// with no texture image re-created or erased, and every attachment still in GENERAL with no
|
|
// pending upload, would resolve exactly these again and continue the pass: it skips both
|
|
// resolves and the pass key. Never for the default framebuffer, whose image index moves
|
|
// with each acquire.
|
|
struct WirePassMemo {
|
|
Bool valid = false;
|
|
Uint64 framebufferSerial = 0, contextSerial = 0;
|
|
MG_Pipe::MGPipeHandle drawFramebuffer = MG_Pipe::kMGPipeNullHandle;
|
|
Uint64 textureImageEpoch = 0, resourceEraseEpoch = 0;
|
|
Bool framebufferSrgb = false;
|
|
Uint64 passHash = 0;
|
|
struct Attachment {
|
|
VkTextureManager::TextureResource* resource = nullptr;
|
|
MG_Pipe::MGPipeHandle storage = MG_Pipe::kMGPipeNullHandle;
|
|
Bool renderbuffer = false;
|
|
};
|
|
Vector<Attachment> attachments;
|
|
Vector<WireDrawScratch::GpuWriteMark> marks;
|
|
};
|
|
WirePassMemo m_wirePassMemo;
|
|
Bool WirePassMemoHolds(const MG_Pipe::MGPFramebufferState& fbo, const FrameContext::FrameData& frame) const;
|
|
Uint64 m_wirePassMarksClearGeneration = 0;
|
|
WireDrawScratch m_wireDrawScratch;
|
|
Uint64 m_wireStreamsConverted = 0, m_wireStreamsReused = 0;
|
|
Bool PrepareWireDrawPass(FrameContext::FrameData& frame, const MG_Pipe::MGPFramebufferState& fbo,
|
|
WireDrawScratch& scratch, Bool passContinues, Bool& continuing);
|
|
// P15: glClear / glClearBuffer* into the attachments of the wire draw pass, recorded inside it
|
|
// (vkCmdClearAttachments). Returns the GL clear-mask bits it could not take; those go through
|
|
// the standalone path.
|
|
GLbitfield ClearWireFramebufferInPass(const MG_Pipe::MGPFramebufferState& fbo,
|
|
const ClearAttachmentPayload& payload, GLint drawbuffer);
|
|
struct WireRetiredObjects {
|
|
Uint64 submitIndex = 0;
|
|
UniquePtr<RenderPassEntry> drawPass;
|
|
Vector<VkFramebuffer> framebuffers;
|
|
Vector<VkRenderPass> renderPasses;
|
|
Vector<VkImageView> imageViews;
|
|
Vector<VkDescriptorPool> descriptorPools;
|
|
// Transfer/resolve scratch storage belongs to the submission that
|
|
// consumes it, just like the views and framebuffer above.
|
|
Vector<UniquePtr<VkTextureManager::TextureResource>> textures;
|
|
Vector<UniquePtr<VkBufferObject>> buffers;
|
|
};
|
|
void RetireWireObjects(WireRetiredObjects objects);
|
|
void CollectWireObjects(Uint64 completedSubmit, Bool all = false);
|
|
Vector<WireRetiredObjects> m_wireRetiredObjects;
|
|
Uint32 m_wirePreparationDepth = 0;
|
|
// P8-D: set only while DrawWireIndirectNative is inside SetupDraw, so SetupWireDraw can
|
|
// tell the wire arm's own indirect draw from a monolith indirect entry point reached under
|
|
// a transport (still the `buffer-legacy-arm` role violation).
|
|
Bool m_wireNativeIndirectDraw = false;
|
|
void DispatchWireCompute(GLuint x, GLuint y, GLuint z);
|
|
// P8-SV: glDispatchComputeIndirect on the wire arm - vkCmdDispatchIndirect from the
|
|
// dispatch-indirect store, as the monolith arm issues it (WireDraw.inc).
|
|
void DispatchWireComputeIndirect(GLintptr offset);
|
|
// False: the device was lost and the session latched; the caller drops its draw/dispatch.
|
|
Bool RewindWireDescriptorSetsIfDue();
|
|
UniquePtr<RenderPassEntry> m_wireDrawPass;
|
|
Vector<VkImageView> m_wireDrawViews;
|
|
VkPipeline GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags,
|
|
const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry,
|
|
Bool primitiveRestartEnable, Uint64 wireRenderPassCompatibilityId = 0);
|
|
|
|
void CopyWireFramebufferToTexture(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
|
GLint x, GLint y, GLsizei width, GLsizei height);
|
|
// ---- Submission fence tracking (GL sync objects) ----
|
|
// One record per vkQueueSubmit still in flight, in ascending submit
|
|
// order. Present/readback submissions reference the frame slot's
|
|
// fence (not pool-owned); mid-frame flushes use pooled fences that are
|
|
// recycled once their submission is observed complete.
|
|
// Not thread-safe: like the rest of the renderer, the tracker relies
|
|
// on GL calls being serialized (launchers migrate the context across
|
|
// threads, but calls never run concurrently), so sync-object polls
|
|
// may mutate it without locking.
|
|
struct SubmitRecord {
|
|
Uint64 submitIndex = 0;
|
|
// Buffer-manager frame serial the submission was made under; its
|
|
// completion raises the completed-serial floor (timer queries and
|
|
// buffer busy-tracking live in frame-serial space).
|
|
Uint64 frameSerial = 0;
|
|
VkFence fence = VK_NULL_HANDLE;
|
|
Bool pooledFence = false;
|
|
};
|
|
// Registers a submission that vkQueueSubmit just made with `fence`.
|
|
// Invariant: every graphics-queue submission that outlives its call
|
|
// site must be registered so GL fences observe it. Exempt are the
|
|
// texture-upload/preserve submits in VkTextureManager, which
|
|
// vkWaitForFences inline before returning.
|
|
void RegisterSubmit(VkFence fence, Bool pooledFence);
|
|
// Builds the submit packet for the frame's pending command buffer
|
|
// (consuming the acquire semaphore on the first submission that
|
|
// references the acquired image, or on any when `waitAcquire`),
|
|
// submits it with `fence`, and registers the submission. On failure
|
|
// the frame state is left untouched. Shared by the mid-frame flush
|
|
// and the readback path so the semaphore-consumption invariant lives
|
|
// in one place.
|
|
Bool SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence,
|
|
Bool waitAcquire = false);
|
|
// Called before the frame first references the acquired swapchain image: submits what is
|
|
// recorded so far WITHOUT the acquire wait, then marks the recording as referencing the
|
|
// image, so only the window pass waits for the presentation engine's release.
|
|
void SubmitAheadOfAcquiredImage();
|
|
// Polls in-flight submission fences (prefix order) and advances the
|
|
// completed counter past every fence observed signaled.
|
|
void RefreshCompletedSubmits();
|
|
// All submissions up to `submitIndex` are known complete (their fence
|
|
// was waited or the device was idled); drops their records and
|
|
// recycles pooled fences.
|
|
void OnSubmitsCompletedUpTo(Uint64 submitIndex);
|
|
// A later fence alone cannot retire earlier submissions. Wait for the
|
|
// whole still-live prefix before advancing the completed submit floor.
|
|
Bool WaitForSubmitsUpTo(Uint64 submitIndex, Uint64 timeoutNs);
|
|
VkFence AcquirePooledSubmitFence();
|
|
void DestroySubmitFencePool();
|
|
Bool HasPendingRecordedWork() const;
|
|
// Frame-boundary housekeeping for paths that never reach Present's
|
|
// tail (present-less readback loops, suspended presentation, blocking
|
|
// sync waits): runs the same per-frame drains Present performs, but
|
|
// only when every queue submission has been observed complete AND no
|
|
// recorded-but-unsubmitted commands exist - i.e. when CPU-GPU overlap
|
|
// is provably already zero. Never blocks (non-blocking fence poll
|
|
// only), so the presenting path's frames-in-flight pipelining is
|
|
// untouched. Returns true when the drain ran.
|
|
Bool TryDrainFrameTransients();
|
|
// Bounded recording: called at the top of every GL draw and dispatch, before anything
|
|
// of that call is recorded. Once the open frame command buffer holds
|
|
// MagmaMaxDrawsPerCommandBuffer draws/dispatches, submits it (FlushPendingCommands) so
|
|
// the call continues on a fresh one, then waits until at most frames-in-flight + 1
|
|
// submissions are outstanding. A driver backs a command buffer with GPU memory it only
|
|
// returns when the buffer is freed (Adreno: one 16 KiB mapping per chunk), so a frame
|
|
// that is never split grows it without bound - rd12's loading frame records ~1M draws
|
|
// into one buffer and runs the process out of vm.max_map_count.
|
|
void SplitOversizedRecording();
|
|
|
|
Vector<SubmitRecord> m_inFlightSubmits;
|
|
Vector<VkFence> m_freeSubmitFences;
|
|
Uint64 m_submitCounter = 0;
|
|
Uint64 m_completedSubmitCounter = 0;
|
|
// IsSubmitIndexComplete's quiet window: the in-flight front last seen unsignaled, and when.
|
|
Uint64 m_unsignaledPollFront = 0;
|
|
Uint64 m_unsignaledPollNs = 0;
|
|
// Drains since the last Present, gating the drain's frame-boundary-equivalent
|
|
// work (arena rewind + cache aging): a presenting app's mid-frame
|
|
// readbacks/waits must neither churn the transient caches nor accelerate the
|
|
// aging clocks, while present-less loops still cross a boundary every few
|
|
// iterations. Reset in Present.
|
|
Uint32 m_drainsSinceLastPresent = 0;
|
|
// GL draws/dispatches recorded into the open frame command buffer; reset whenever a
|
|
// recording begins (OnFrameCommandRecordingBegan). See SplitOversizedRecording.
|
|
Uint32 m_drawsInRecording = 0;
|
|
Uint64 m_oversizedRecordingSplits = 0;
|
|
|
|
NativeWindowType m_window = 0;
|
|
void* m_platformDisplay = nullptr;
|
|
void* m_platformLibrary = nullptr;
|
|
void* m_platformCloseDisplay = nullptr;
|
|
// Whether the loader exposes VK_EXT_headless_surface, detected once in
|
|
// CreateInstance() from the enumerated instance extensions. On desktop an
|
|
// offscreen surface REQUIRES it: false is a clean, loud bring-up failure, never
|
|
// a substituted window. (Android is the one exception and has its own path -
|
|
// no Mali/Adreno driver seen so far exposes the extension, so a windowless
|
|
// context is given an AImageReader ANativeWindow that is never displayed.)
|
|
// ENABLED ON THIS INSTANCE, not merely offered: a renderer created for a window never asks
|
|
// for the extension, and its later offscreen targets must not call an entry point the
|
|
// instance does not have (OffscreenSurfaceRoute.h). Hence false until CreateInstance says so.
|
|
Bool m_headlessSurfaceSupported = false;
|
|
// VK_KHR_android_surface was enabled on this instance (the AImageReader route needs it).
|
|
Bool m_androidSurfaceEnabled = false;
|
|
// Android has the same shortfall: no Mali/Adreno driver seen so far exposes
|
|
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
|
|
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
|
// displayed - the reader's images are simply never acquired. Owned here, so
|
|
// Shutdown() deletes it.
|
|
void* m_fallbackImageReader = nullptr;
|
|
VulkanRendererConfig m_config;
|
|
// Constructed for the app's window rather than a pbuffer (m_window is later filled in
|
|
// for the pbuffer fallbacks too, so it cannot tell them apart).
|
|
Bool m_presentsToAppWindow = false;
|
|
// The m_config.SwapInterval the live swapchain was built for; Present compares the two.
|
|
Optional<Int> m_swapchainSwapInterval;
|
|
Bool m_swapchainResizeRequested = false;
|
|
// Presentation is suspended while the window is zero-area (minimized): the
|
|
// swapchain is unusable/out of date, so Present drops frames instead of
|
|
// submitting on a signaled fence / presenting never-acquired images.
|
|
Bool m_presentSuspended = false;
|
|
|
|
// Vulkan objects
|
|
Bool m_validationLayersEnabled = false;
|
|
Vector<VkExtensionProperties> m_extensions;
|
|
VkInstance m_instance = VK_NULL_HANDLE;
|
|
VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE;
|
|
// Fallback reporting channel for drivers that ship the validation layers but
|
|
// only expose the older VK_EXT_debug_report (Adreno 650 / Vulkan 1.1.128).
|
|
VkDebugReportCallbackEXT m_debugReportCallback = VK_NULL_HANDLE;
|
|
PhysicalDevice m_physicalDevice;
|
|
VkDevice m_device = VK_NULL_HANDLE;
|
|
VmaAllocator m_allocator = nullptr;
|
|
VkSurfaceKHR m_surface = VK_NULL_HANDLE;
|
|
SwapchainObject m_swapchainObject;
|
|
|
|
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
|
VkQueue m_presentQueue = VK_NULL_HANDLE;
|
|
Bool m_drawIndirectCountExtensionEnabled = false;
|
|
Bool m_indexTypeUint8ExtensionEnabled = false;
|
|
Bool m_logicOpFeatureEnabled = false;
|
|
Bool m_multiDrawIndirectFeatureEnabled = false;
|
|
// drawIndirectFirstInstance gates indirect commands whose firstInstance != 0;
|
|
// cached at device creation because the tier-2 multi-draw path (a transient
|
|
// VkDrawIndexedIndirectCommand array) is illegal for such a sub-draw without it.
|
|
Bool m_drawIndirectFirstInstanceFeatureEnabled = false;
|
|
// VK_EXT_multi_draw: native batched submission for the CPU-side glMultiDraw*
|
|
// families (tier 1 of the multi-draw dispatch).
|
|
Bool m_multiDrawExtensionEnabled = false;
|
|
Uint32 m_maxMultiDrawCount = 0;
|
|
// Multi-draw dispatch tiers, resolved once at device creation from device support
|
|
// clamped by MOBILEGL_MAGMA_MULTIDRAW_MODE (a preference, never a demand):
|
|
// tier 1 (ext): one vkCmdDrawMulti(Indexed)EXT - m_multiDrawAllowExt
|
|
// tier 2 (indirect): one vkCmdDraw(Indexed)Indirect batch - m_multiDrawAllowIndirect
|
|
// tier 3 (unroll): one vkCmdDraw(Indexed) per sub-draw - always available
|
|
// m_multiDrawForceUnrollIndirect additionally forces the GPU-parameter
|
|
// glMultiDraw*Indirect paths onto their per-command loop (mode=unroll only).
|
|
Bool m_multiDrawAllowExt = false;
|
|
Bool m_multiDrawAllowIndirect = false;
|
|
Bool m_multiDrawForceUnrollIndirect = false;
|
|
Bool m_samplerAnisotropyFeatureEnabled = false;
|
|
Bool m_shaderDrawParametersExtensionEnabled = false;
|
|
Bool m_shaderDrawParametersFeatureEnabled = false;
|
|
// Native subgroup topology, queried at device creation for the compute-module
|
|
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
|
|
// stage flag; 0 / false when the device has no usable compute subgroups or
|
|
// MOBILEGL_MAGMA_DISABLE_SUBGROUP forced them off.
|
|
Uint32 m_nativeSubgroupSize = 0;
|
|
Bool m_nativeSubgroupSupported = false;
|
|
Bool m_computeFullSubgroupsFeatureEnabled = false;
|
|
// VkPhysicalDeviceSubgroupSizeControlProperties::maxComputeWorkgroupSubgroups;
|
|
// 0 when the extension (and therefore the full-subgroups flag) is unavailable.
|
|
Uint32 m_maxComputeWorkgroupSubgroups = 0;
|
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
|
// Set only after descriptor-indexing feature AND property queries prove that
|
|
// update-after-bind is legal for every descriptor category this renderer emits.
|
|
ProgramFactory::UpdateAfterBindLimits m_updateAfterBindLimits{};
|
|
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
|
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
|
// drive a runtime fallback when the device lacks them.
|
|
Bool m_fillModeNonSolidFeatureEnabled = false;
|
|
Bool m_independentBlendFeatureEnabled = false;
|
|
// depthBiasClamp gates the clamp argument of vkCmdSetDepthBias (glPolygonOffsetClamp);
|
|
// without it the polygon offset is applied unclamped.
|
|
Bool m_depthBiasClampFeatureEnabled = false;
|
|
// dualSrcBlend gates GL_SRC1_* blend factors (glBindFragDataLocationIndexed dual-source blend);
|
|
// primitiveTopologyListRestart gates primitive restart on *list* topologies (strip/fan restart
|
|
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
|
Bool m_dualSrcBlendFeatureEnabled = false;
|
|
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
|
// shaderTessellationAndGeometryPointSize gates the PointSize built-in in a tessellation
|
|
// or geometry stage, which desktop GL treats as an ordinary per-vertex output (writable,
|
|
// and capturable by name through transform feedback). Cached at device creation and
|
|
// handed to ProgramFactory, which refuses a program whose tessellation or geometry module
|
|
// declares the matching SPIR-V capability while this is false - SetupDraw then skips its
|
|
// draws (VkProgramObject::pointSizeCapabilityUnsupported) rather than building a pipeline
|
|
// that is invalid usage.
|
|
Bool m_tessellationAndGeometryPointSizeFeatureEnabled = false;
|
|
// VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
|
|
// transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
|
|
// an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
|
|
// colour outside the palette has to be snapped to the nearest predefined one. Both features
|
|
// are required together: customBorderColorWithoutFormat is what lets a sampler carry a custom
|
|
// colour without naming the image format it will be paired with, which GL's sampler objects
|
|
// cannot know. maxCustomBorderColorSamplers is a real device limit, so the sampler cache has
|
|
// to be able to fall back to the snapped value once it is reached.
|
|
Bool m_customBorderColorFeatureEnabled = false;
|
|
Uint32 m_maxCustomBorderColorSamplers = 0;
|
|
// sampleRateShading gates VkPipelineMultisampleStateCreateInfo::sampleShadingEnable, i.e.
|
|
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading. Unlike dualSrcBlend this does NOT
|
|
// hard-fail the draw when absent: sample shading is a rate hint, and every sample-rate
|
|
// pipeline is still correct (just not per-sample) at the default rate - so the enable is
|
|
// dropped and the draw proceeds, which is what a GL implementation with SAMPLES=1 does too.
|
|
Bool m_sampleRateShadingFeatureEnabled = false;
|
|
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
|
|
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
|
|
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
|
|
// declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
|
|
Bool m_multiViewportFeatureEnabled = false;
|
|
Uint32 m_maxRasterizableViewports = 1;
|
|
// Union of shader stages sampled-read barriers may name; built at device creation
|
|
// because geometry/tessellation stage bits are invalid in a barrier when their
|
|
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
|
// ALL_GRAPHICS would also serialize against non-shader stages.
|
|
VkPipelineStageFlags m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
|
// Cached at device creation from the graphics queue family properties
|
|
// and device limits; drives timer-query support.
|
|
Uint32 m_timestampValidBits = 0;
|
|
Float m_timestampPeriodNs = 0.0f;
|
|
Bool m_timerQuerySupported = false;
|
|
using PFNDrawIndexedIndirectCountFunc = void(VKAPI_PTR*)(VkCommandBuffer commandBuffer, VkBuffer buffer,
|
|
VkDeviceSize offset, VkBuffer countBuffer,
|
|
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
|
|
Uint32 stride);
|
|
PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
|
// P8-D: vkCmdDrawIndirectCount (same signature, same extension), for the wire arm's
|
|
// glMultiDrawArraysIndirectCount. The monolith arm never loads it: it reads that count on
|
|
// the CPU (DirectVulkan.cpp's MultiDrawArraysIndirectCount).
|
|
PFNDrawIndexedIndirectCountFunc s_vkCmdWireDrawIndirectCount = nullptr;
|
|
// VK_EXT_multi_draw entry points, loaded at device creation when the extension
|
|
// (and its multiDraw feature) is enabled; null otherwise.
|
|
PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
|
|
PFN_vkCmdDrawMultiIndexedEXT s_vkCmdDrawMultiIndexedEXT = nullptr;
|
|
// VK_KHR_fragment_shading_rate, which backs GL_EXT_fragment_shading_rate: what was enabled
|
|
// at device creation and what the device answers for it (see GetFragmentShadingRateSupport).
|
|
// Null / all-false on a device without pipelineFragmentShadingRate.
|
|
PFN_vkCmdSetFragmentShadingRateKHR s_vkCmdSetFragmentShadingRateKHR = nullptr;
|
|
FragmentShadingRateSupport m_fragmentShadingRate{};
|
|
|
|
// VK_EXT_transform_feedback (GL transform feedback capture)
|
|
Bool m_transformFeedbackFeatureEnabled = false;
|
|
// VK_EXT_provoking_vertex. Vulkan's built-in convention is "provoking vertex first"; GL's
|
|
// default is LAST_VERTEX_CONVENTION, and GL derives BOTH flat shading and the transform
|
|
// feedback vertex order from it. provokingVertexLast alone fixes flat shading and the
|
|
// input-assembler capture order and has no dependency on transform feedback; only
|
|
// transformFeedbackPreservesProvokingVertex does.
|
|
Bool m_provokingVertexLastEnabled = false;
|
|
// transformFeedbackPreservesProvokingVertex was actually enabled at device creation. Kept
|
|
// separate because it is the only thing that arms
|
|
// VUID-VkGraphicsPipelineCreateInfo-topology-04884, the rule that forbids a TRIANGLE_FAN
|
|
// pipeline from asking for LAST on a device that cannot preserve a fan's provoking vertex.
|
|
Bool m_provokingVertexXfbPreserveEnabled = false;
|
|
// provokingVertexModePerPipeline: when VK_FALSE every pipeline in one render pass instance
|
|
// must agree on the mode, so glProvokingVertex(GL_FIRST_VERTEX_CONVENTION) cannot be honoured
|
|
// per draw and every pipeline takes GL's default (LAST) instead.
|
|
Bool m_provokingVertexModePerPipeline = false;
|
|
// transformFeedbackPreservesTriangleFanProvokingVertex.
|
|
Bool m_provokingVertexFanPreserved = false;
|
|
// Per-pipeline provoking-vertex mode. capturesXfbFromGeometryStage must be a LINK-TIME
|
|
// property of the program, never the dynamic "is transform feedback active" flag: the
|
|
// 8-entry m_pipelineMemo and the SetupDrawSnapshot fast path key on programObj.hash and
|
|
// the pipeline-state value hash, neither of which moves when glBeginTransformFeedback is
|
|
// called, so a dynamic input here would hand back a stale VkPipeline.
|
|
VkProvokingVertexModeEXT SelectProvokingVertexMode(VkPrimitiveTopology topology,
|
|
Bool capturesXfbFromGeometryStage) const;
|
|
// VK_EXT_vertex_attribute_divisor: without it every non-zero glVertexAttribDivisor
|
|
// behaves as 1, because that is all Vulkan's instance input rate can express.
|
|
Bool m_vertexAttributeDivisorEnabled = false;
|
|
PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
|
|
PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
|
|
PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
|
|
// Counter buffers (one 4-byte slot per capture binding) let consecutive
|
|
// draws within one glBeginTransformFeedback append GL-style. Transform feedback
|
|
// objects can each hold an open, paused span at the same time, so the counters are
|
|
// per object: one group of four slots each, handed out on first use.
|
|
static constexpr SizeT kXfbCounterObjectSlots = 16;
|
|
VkBufferObject m_xfbCounterBuffer;
|
|
// Which transform feedback object owns each slot group, by the frontend's never-reused
|
|
// lifetime id (0 = the slot is free). This used to be an UnorderedMap keyed on the GL
|
|
// NAME, which is recycled by glGenTransformFeedbacks: a deleted-and-recreated object
|
|
// inherited the dead one's slot, and since nothing ever removed an entry the map also
|
|
// grew for the life of the context. A fixed table cannot do either: a group is taken over
|
|
// only from an owner with no OPEN span (see CurrentXfbCounterSlot), so an object whose
|
|
// counters can still be resumed never loses them, and a dead object's group comes back.
|
|
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotOwner{};
|
|
// Tie-break among reclaimable groups only; never on its own, because the paused span the
|
|
// groups exist for is by construction the least recently used one.
|
|
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotLastUse{};
|
|
Uint64 m_xfbCounterSlotUseSerial = 0;
|
|
// Set for a slot once a captured draw has been recorded into its span; selects
|
|
// counter-buffer resume on the next captured draw of the same span.
|
|
Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
|
|
Array<Uint64, kXfbCounterObjectSlots> m_xfbLastSeenGeneration{};
|
|
// Counter slot group of the bound transform feedback object.
|
|
Uint32 CurrentXfbCounterSlot();
|
|
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
|
|
// when GL transform feedback is active; binds capture buffers on demand.
|
|
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
|
|
Bool BeginXfbCaptureWithBuffers(FrameContext::FrameData& frame, Uint32 count,
|
|
const VkBuffer* buffers, const VkDeviceSize* offsets,
|
|
const VkDeviceSize* sizes);
|
|
Bool BeginWireXfbCaptureForDraw(FrameContext::FrameData& frame);
|
|
Uint32 m_currentDrawXfbBufferCount = 0;
|
|
Uint32 m_currentDrawXfbBufferMask = 0;
|
|
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
|
|
// Makes the captured bytes visible to whatever reads them next. Deferred rather than
|
|
// recorded next to the capture, because the capturing draw runs inside a render pass
|
|
// that declares no self-dependency.
|
|
void MakeXfbWritesVisible();
|
|
Bool m_xfbWritesPendingVisibility = false;
|
|
// Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
|
|
// query is active. Returns whether a slot was begun (End must mirror it).
|
|
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
|
|
void EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began);
|
|
Bool m_occlusionQueryPreciseEnabled = false;
|
|
Bool m_hostQueryResetEnabled = false;
|
|
PFN_vkResetQueryPool s_vkResetQueryPool = nullptr;
|
|
VkQueryPool m_occlusionQueryPool = VK_NULL_HANDLE;
|
|
static constexpr Uint32 kOcclusionQuerySlots = 8192;
|
|
Uint32 m_occlusionSlotCursor = 0;
|
|
Bool m_occlusionCaptureActive = false;
|
|
Vector<Uint32> m_occlusionActiveSlots;
|
|
// Transform feedback primitive queries: one pool slot per captured draw yields
|
|
// the (written, needed) pair; GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN sums the
|
|
// first, GL_PRIMITIVES_GENERATED the second - exact with geometry shaders,
|
|
// unlike the CPU fallback accounting.
|
|
Bool m_xfbQueriesSupported = false;
|
|
PFN_vkCmdBeginQueryIndexedEXT s_vkCmdBeginQueryIndexedEXT = nullptr;
|
|
PFN_vkCmdEndQueryIndexedEXT s_vkCmdEndQueryIndexedEXT = nullptr;
|
|
VkQueryPool m_xfbQueryPool = VK_NULL_HANDLE;
|
|
static constexpr Uint32 kXfbQuerySlots = 8192;
|
|
Uint32 m_xfbQuerySlotCursor = 0;
|
|
Bool m_xfbQueryCaptureActive[2] = {false, false}; // [0]=written, [1]=generated
|
|
Vector<Uint32> m_xfbQueryActiveSlots[2];
|
|
Bool m_xfbQuerySlotOpen = false;
|
|
Uint32 m_xfbQueryOpenSlot = 0;
|
|
// GL_PRIMITIVES_GENERATED reroute for draws made while transform feedback is
|
|
// INACTIVE. The stream pool's primitivesNeeded is defined to count those draws
|
|
// too, but a Mali driver (and Mesa lavapipe) answers 0 unless a capture span
|
|
// is open (the CTS's tessellator-measuring shape). Where the bring-up probe
|
|
// finds that defect with a working control - or
|
|
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE forces it - such draws accumulate the
|
|
// GENERATED count through this pool instead, whose type the arming picks:
|
|
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT where the device hosts the dedicated
|
|
// query with its rasterizer-discard feature (exact semantics by definition -
|
|
// the extension exists because GL needs this count without a capture), else a
|
|
// VK_QUERY_TYPE_PIPELINE_STATISTICS pool over clipping-stage invocations (one
|
|
// per primitive reaching primitive clipping - after every vertex processing
|
|
// stage, before rasterizer discard - which is the same set).
|
|
// XFB-ACTIVE draws keep the stream slot (exact today, and WRITTEN needs it);
|
|
// every draw with no open capture - a PAUSED span's draws included - takes a
|
|
// reroute slot, and the span then ignores the frontend's CPU paused-primitive
|
|
// counter rather than adding it on top (see IsPrimGenRerouteArmed): that
|
|
// counter is written by only 3 of the ~15 draw entry points and answers 0 for
|
|
// GL_PATCHES, so it cannot price the draws this reroute exists to repair. One
|
|
// GL query span may therefore hold slots of both pools.
|
|
Bool m_pipelineStatisticsQueryFeatureEnabled = false;
|
|
// VK_EXT_primitives_generated_query: base feature, and the
|
|
// ...WithRasterizerDiscard feature without which a discarding draw inside the
|
|
// query is invalid usage (so the reroute never picks the dedicated pool on a
|
|
// base-only device - GL applications toggle discard freely).
|
|
Bool m_primitivesGeneratedQueryFeatureEnabled = false;
|
|
Bool m_primitivesGeneratedQueryDiscardFeatureEnabled = false;
|
|
// tessellationShader was enabled at device creation (it is taken whenever the
|
|
// device advertises it); gates the probe's PATCHES shape.
|
|
Bool m_tessellationShaderFeatureEnabled = false;
|
|
MG_Util::SelfTest::PrimGenRerouteKind m_primGenRerouteKind =
|
|
MG_Util::SelfTest::PrimGenRerouteKind::None;
|
|
// The bring-up probe measured this device's stream query as counting draws made
|
|
// with no capture span open (the StreamCounts verdict) - so it counts the
|
|
// PAUSED-span ones too, through the stream slot they take when nothing is
|
|
// rerouted. Only the probe can know this, so it stays false wherever the probe
|
|
// is not consulted (the forced arms), which keeps those lanes' accounting as it
|
|
// was.
|
|
Bool m_primGenStreamCountsXfbInactiveDraws = false;
|
|
VkQueryPool m_primGenReroutePool = VK_NULL_HANDLE;
|
|
Uint32 m_primGenRerouteSlotCursor = 0;
|
|
Vector<Uint32> m_primGenRerouteActiveSlots;
|
|
Bool m_primGenRerouteSlotOpen = false;
|
|
Uint32 m_primGenRerouteOpenSlot = 0;
|
|
// Runs the bring-up probe (memoized per process) and decides
|
|
// m_primGenRerouteKind. Called at the end of device creation: it records on
|
|
// m_graphicsQueue, which nothing else is using yet.
|
|
void ArmPrimGenReroute();
|
|
|
|
public:
|
|
// Whether a GENERATED span opened now will have the draws made while the GL
|
|
// span is PAUSED counted on the GPU - through the reroute pool, which takes
|
|
// every draw with no open capture, or (where the reroute is not armed because
|
|
// the stream query was measured to count capture-less draws) through the stream
|
|
// slot such a draw still takes. The frontend's CPU paused-primitive counter
|
|
// must not be added on top of either: it would double count, and it cannot
|
|
// price the draws that matter anyway - only 3 of the ~15 draw entry points
|
|
// write it and it answers 0 for GL_PATCHES. Read once per span, after
|
|
// StartXfbQueryCapture (whose pool creation may disarm the reroute).
|
|
Bool ArePausedDrawsGpuCounted() const;
|
|
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
|
|
Bool StartXfbQueryCapture(Uint32 kind);
|
|
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots, Vector<Uint32>& outRerouteSlots);
|
|
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
|
|
Bool wantGenerated, Uint64& outPrimitives);
|
|
|
|
private:
|
|
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive);
|
|
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
|
|
|
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
|
|
|
VkBufferManager m_bufferManager;
|
|
|
|
Uint m_imageIndexAcquired = 0;
|
|
// The swapchain image GL addresses as the default framebuffer: the one a readback, a
|
|
// blit source or a copy out of it resolves, and the one the app last rendered into.
|
|
// Present() runs ahead - it presents the frame and acquires the next image without
|
|
// waiting for the apply that would render into it (kWaitPresent) - so this must NOT
|
|
// follow m_imageIndexAcquired. Before it existed a readback resolved the freshly
|
|
// acquired image, which nothing had written yet: the pure-black sundial-lite frame.
|
|
Uint m_defaultFramebufferImageIndex = 0;
|
|
FrameContext m_frameContext;
|
|
|
|
// Resolves the swapchain image index a READ of the default framebuffer must use. A
|
|
// non-default framebuffer ignores the index; it keeps the acquired one, which still
|
|
// keys its render-pass cache.
|
|
Uint32 DefaultFramebufferReadIndex(Bool isDefaultFramebuffer) const {
|
|
return isDefaultFramebuffer ? m_defaultFramebufferImageIndex : m_imageIndexAcquired;
|
|
}
|
|
|
|
// Same for a WRITE: the default framebuffer starts addressing the image Present()
|
|
// acquired only once something renders into it again.
|
|
Uint32 DefaultFramebufferWriteIndex(Bool isDefaultFramebuffer) {
|
|
if (isDefaultFramebuffer) m_defaultFramebufferImageIndex = m_imageIndexAcquired;
|
|
return DefaultFramebufferReadIndex(isDefaultFramebuffer);
|
|
}
|
|
|
|
UniquePtr<PipelineFactory> m_pipelineFactory;
|
|
// Single-slot "last pipeline" memo: skip the per-draw GetOrCreatePipeline work (state
|
|
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
|
|
// state is unchanged from the previous draw. The key provably covers every pipeline field.
|
|
// Reset per-frame and on pipeline destruction so the cached handle can never dangle.
|
|
// Small N-way pipeline-resolution memo (round-robin replacement). A
|
|
// single-entry memo thrashed on draw sequences that alternate a few
|
|
// pipelines (GUI text/quad program ping-pong), paying the full
|
|
// payload-hash lookup per draw; eight entries cover such working sets
|
|
// while keeping the hit path a trivial linear scan.
|
|
struct PipelineMemoEntry {
|
|
GLenum mode = 0;
|
|
Uint64 programHash = 0;
|
|
Uint64 vertexInputHash = 0;
|
|
Uint64 renderPassHash = 0;
|
|
Uint64 wireRenderPassCompatibilityId = 0;
|
|
// The PRE-HANDLE arm's key component (P2 brief D12.1), and 0 in every entry the
|
|
// handle arm mints. VALUE hash of the pipeline-relevant fixed-function state (see
|
|
// ComputePipelineStateHash), not the monotonic pipeline-state version: the version
|
|
// never repeats, so a per-draw GL_BLEND toggle would miss all entries forever even
|
|
// though the state alternates between two values the memo already holds.
|
|
Uint64 pipelineStateHash = 0;
|
|
// The HANDLE arm's key component, and the whole of D12.1: the CLIENT already
|
|
// hashed the pipeline subset of RenderStateParameters and minted a content-
|
|
// addressed CSO for it (MG_Pipe/MGPipeRenderStateSpans.h, MG_Impl/Pipe/CsoCache),
|
|
// so re-hashing the same 396 bytes here was work the boundary had already done.
|
|
// Two draws share a CSO handle exactly when their pipeline bytes are equal, and
|
|
// the client's subset is a strict SUPERSET of what ComputePipelineStateHash read,
|
|
// so the handle discriminates at least as finely as the hash it replaces.
|
|
//
|
|
// renderPassHash STAYS beside it and is what keeps this key complete: the CSO
|
|
// carries GL state only, while colorAttachmentCount and the rasterization sample
|
|
// count - which ComputePipelineStateHash folded in through its signature and
|
|
// through ResolveEffectiveSampleMask - are render-pass facts that the render-pass
|
|
// hash already separates.
|
|
//
|
|
// Null in an entry minted by the legacy arm, so entries of the two arms can never
|
|
// match each other: the compare below tests BOTH components.
|
|
MG_Pipe::MGPipeHandle renderStateCso = MG_Pipe::kMGPipeNullHandle;
|
|
ProgramFactory::CompileOptionFlags transformFlags = {};
|
|
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
|
|
// from anything else in this key: it depends on whether the draw is indexed and on the
|
|
// index type, neither of which the mode/program/state hashes carry. Without it an
|
|
// indexed and a non-indexed draw over the same program and state collide on one entry
|
|
// and the second one gets the first one's restart setting.
|
|
Bool primitiveRestartEnable = false;
|
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
|
};
|
|
static constexpr Uint32 kPipelineMemoSize = 8;
|
|
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
|
Uint32 m_pipelineMemoCount = 0;
|
|
Uint32 m_pipelineMemoNext = 0;
|
|
|
|
// P2 D12.1's arm selector, and the whole of the pipeline memo's re-key. Returns the
|
|
// render-state CSO this draw is keyed on, or the null handle when the pre-handle arm
|
|
// is the one that runs.
|
|
//
|
|
// Under the handle arm the memo's state key IS this handle. The client hashed those
|
|
// 396 pipeline bytes when it minted the CSO (MGPipeComputePipelineSubsetHash), so
|
|
// recomputing an overlapping hash here was work the boundary had already done; the
|
|
// client's pipeline subset is a strict SUPERSET of what ComputePipelineStateHash read,
|
|
// so the handle discriminates at least as finely as the hash it replaces. What the
|
|
// handle does NOT carry is the render-pass side - colorAttachmentCount and the
|
|
// rasterization sample count, which ComputePipelineStateHash folded in through its
|
|
// signature and through ResolveEffectiveSampleMask - and that is exactly why
|
|
// entry.renderPassHash stays in the key beside it.
|
|
//
|
|
// The arm is live only when the render-state subsystem is migrated in this run AND the
|
|
// client has actually bound a CSO. The second half is not belt and braces: a tree whose
|
|
// tracker does not emit create/bind_render_state yet has no handle to key on, and
|
|
// delete_render_state clears the binding (MG_Pipe/PipeApply.cpp), so the null handle is
|
|
// reachable on any tree. Keying every draw on it would alias every render state onto
|
|
// one memo entry, so a null handle means "fall back to a state hash" - never an abort,
|
|
// and never a per-draw consultation of the legacy-memo lever: bit 0 is not a Track-H
|
|
// subsystem (D14 labels only bits 5 and 6 that), and the lever's Fatal is a STARTUP
|
|
// one, in MagmaPipeValidateSubsystemConfiguration.
|
|
//
|
|
// The fallback is warned ONCE rather than logged at debug, and that is deliberate: a
|
|
// silent fallback is what makes "the CSO arm never ran" easy to miss. W is compiled in
|
|
// at every shipped log level.
|
|
//
|
|
// The latch is a plain member bool, NOT MGLOG_W_ONCE. MOBILEGL_LOG_ONCE_INTERNAL
|
|
// (MG_Util/Debug/Log.h) is an UNCONDITIONAL std::atomic_flag::test_and_set - a locked
|
|
// xchg, executed on every evaluation, not "one static bool test" as an earlier round of
|
|
// this comment claimed - and this site is on the per-draw pipeline path in the very
|
|
// configuration that reaches it (no tracker: every draw). ROADMAP.md:7 forbids leaving
|
|
// instrumentation on a hot path, so the once-ness is one non-atomic, always-predicted
|
|
// load of a member that is false exactly once. Single-threaded like the rest of the
|
|
// renderer, and per renderer rather than per process, which is also the right scope: a
|
|
// second context that never binds a CSO deserves to say so.
|
|
//
|
|
// What the absence of this warning from a run's log proves, EXACTLY: that no draw took
|
|
// the fallback WHILE bit 0 was set. With kMGPipeSubsystemRenderState clear the function
|
|
// returns before the latch, so absence proves nothing at all - and no draw is keyed on a
|
|
// handle either. Grep the mask out of the log beside it (review v2 minor 3).
|
|
//
|
|
// Push-only by construction: the pull build does not compile this function at all, so
|
|
// its two callers are statement-for-statement what they were (G1).
|
|
//
|
|
// [routed to the integrator, review v2 minor 11] MG_Pipe::MGPipeApplier() is ONE
|
|
// process-global applier (MG_Pipe/PipeApply.cpp), not the per-context CSO store D2
|
|
// specifies. In a multi-context process this reads whatever CSO another context last
|
|
// bound. The defect is package A's and the fix belongs there; Magma is its only P2
|
|
// consumer, so it is named here rather than left for both reviews to assume the other
|
|
// caught it.
|
|
MG_Pipe::MGPipeHandle ResolveBoundRenderStateCso() const {
|
|
if (!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
|
return MG_Pipe::kMGPipeNullHandle;
|
|
}
|
|
const MG_Pipe::MGPipeHandle boundCso = MG_Pipe::MGPipeApplier().BoundRenderStateCso;
|
|
if (MG_Pipe::MGPipeHandleIsNull(boundCso) && !m_pipelineCsoFallbackWarned) {
|
|
m_pipelineCsoFallbackWarned = true;
|
|
MGLOG_W("MGPipe: kMGPipeSubsystemRenderState is on but no render-state CSO is "
|
|
"bound; the pipeline memo is running on a state hash, not on the CSO "
|
|
"handle (no tracker on this build, or a draw between "
|
|
"delete_render_state and the next bind)");
|
|
}
|
|
return boundCso;
|
|
}
|
|
// Latch for the warning above. Mutable because the resolve is const and the latch is
|
|
// not part of the renderer's observable state.
|
|
mutable Bool m_pipelineCsoFallbackWarned = false;
|
|
// The memo key's STATE-HASH half, for a draw that has no CSO handle to key on: the
|
|
// pre-handle arm, and the fallback of D12.1's handle arm. Cached on the pipeline-state
|
|
// version plus the two render-pass facts the hash's inputs depend on, so an unchanged
|
|
// (version, colorAttachmentCount, sampleCount) proves the bytes are unchanged.
|
|
//
|
|
// [deviation from D12.1] The brief deletes this gate and its cached fields outright.
|
|
// They cannot go while a no-CSO draw is reachable - and it is, on any tree: a draw
|
|
// between delete_render_state and the next bind has no handle. On a tree whose tracker
|
|
// binds a CSO these five words are written once and never read again; they retire for
|
|
// real when the pull path does, at P13.
|
|
Uint64 ResolveFallbackPipelineStateHash(Uint renderStateVersion, Uint32 colorAttachmentCount,
|
|
VkSampleCountFlagBits rasterizationSamples) {
|
|
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
|
m_pipelineStateHashColorCount != colorAttachmentCount ||
|
|
m_pipelineStateHashSampleCount != rasterizationSamples) {
|
|
m_pipelineStateHash = ComputePipelineSubsetStateHashFallback();
|
|
m_pipelineStateHashVersion = renderStateVersion;
|
|
m_pipelineStateHashColorCount = colorAttachmentCount;
|
|
m_pipelineStateHashSampleCount = rasterizationSamples;
|
|
m_pipelineStateHashValid = true;
|
|
}
|
|
return m_pipelineStateHash;
|
|
}
|
|
// The same answer as ComputePipelineStateHash, computed from the P2 chunk table
|
|
// instead of from a hand-written field list, for the build that compiles no
|
|
// pre-handle arm (cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF). It is the CLIENT's own
|
|
// hash function - MGPipeComputePipelineSubsetHash over the 396 pipeline bytes - so a
|
|
// draw keyed on it and a draw keyed on a CSO handle are keyed on the same equivalence
|
|
// class of state, and the render-pass facts stay separated by renderPassHash either
|
|
// way. This is what makes the no-legacy build RUNNABLE rather than a configuration
|
|
// that aborts on the first draw that arrives without a CSO.
|
|
Uint64 ComputePipelineSubsetStateHashFallback() const;
|
|
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
|
|
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
|
|
// and the pipeline-state memo word so the two cannot disagree. NOT part of the legacy
|
|
// arm: it is a PAYLOAD computation that depends on rasterizationSamples, so it survives
|
|
// the re-key and keeps reading Multisample / SampleMask / SampleMaskValue out of the
|
|
// working block.
|
|
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
|
|
// ResolveFallbackPipelineStateHash's cache. Written once and never read again on a
|
|
// build whose client binds a render-state CSO; see that function for why it survives
|
|
// the re-key at all.
|
|
Uint m_pipelineStateHashVersion = 0;
|
|
Uint32 m_pipelineStateHashColorCount = 0;
|
|
// The sample count the cached hash was computed at. A pipeline-state input now depends on
|
|
// it (the effective sample mask), so a draw that changes only the target's sample count
|
|
// has to recompute rather than reuse.
|
|
VkSampleCountFlagBits m_pipelineStateHashSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
Uint64 m_pipelineStateHash = 0;
|
|
Bool m_pipelineStateHashValid = false;
|
|
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
|
|
// function also reads whether the bound DRAW framebuffer is the default one
|
|
// (only the default framebuffer gets the Y-flip and rotation bits - an FBO
|
|
// pass renders unflipped). Keyed on BOTH inputs; missing the FBO bit shipped
|
|
// an upside-down default-framebuffer pass after any render-to-texture
|
|
// (minecraft-1.17-main-menu retrace, whole frame flipped).
|
|
VkSurfaceTransformFlagBitsKHR m_baseTransformFlagsPreTransform =
|
|
VK_SURFACE_TRANSFORM_FLAG_BITS_MAX_ENUM_KHR;
|
|
Bool m_baseTransformFlagsIsDefaultFbo = false;
|
|
Bool m_baseTransformFlagsKeyValid = false;
|
|
Uint32 m_baseTransformFlagsCache = 0;
|
|
// isDefaultFbo must be the default-ness of the CURRENTLY bound draw framebuffer;
|
|
// every caller already has it in hand from its own guards.
|
|
Uint32 GetBaseTransformFlagsRaw(Bool isDefaultFbo);
|
|
// Drops every memoized pipeline handle. Required at command-buffer
|
|
// boundaries and whenever any pipeline may have been destroyed. Also drops
|
|
// the cached pipeline-state hash: the same boundaries can retire the GL
|
|
// context whose monotonic version the cache is keyed on. The handle arm has no
|
|
// such cache to drop - a CSO handle is not derived from a monotonic version.
|
|
void InvalidatePipelineMemo() {
|
|
m_pipelineMemoCount = 0;
|
|
m_pipelineMemoNext = 0;
|
|
m_pipelineStateHashValid = false;
|
|
}
|
|
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
|
UniquePtr<ProgramFactory> m_programFactory;
|
|
UniquePtr<UniformManager> m_uniformManager;
|
|
UniquePtr<VertexInputStateFactory> m_vertexInputStateFactory;
|
|
UniquePtr<VkRenderPassManager> m_renderPassManager;
|
|
UniquePtr<VkTextureManager> m_textureManager;
|
|
UniquePtr<VkSamplerManager> m_samplerManager;
|
|
UniquePtr<VkTimerQueryManager> m_timerQueryManager;
|
|
BlitResources m_blitResources;
|
|
DepthMipmapResources m_depthMipmapResources;
|
|
Vector<DeferredDepthMipmapCleanup> m_deferredDepthMipmapCleanup;
|
|
|
|
// Set from the draw's resolved VkProgramObject on both the full and the fast setup paths;
|
|
// read by BeginXfbCaptureForDraw, which has only GL state otherwise. See
|
|
// VkProgramObject::xfbCaptureDeclined.
|
|
Bool m_currentDrawXfbCaptureDeclined = false;
|
|
|
|
// Per-draw scratch buffer (clear keeps capacity) - this path runs for every draw call and
|
|
// must not allocate.
|
|
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
|
|
|
|
|
void CreateInstance();
|
|
VkResult SetupDebugMessenger();
|
|
VkResult DestroyDebugMessenger();
|
|
VkResult SetupDebugReportCallback();
|
|
void DestroyDebugReportCallback();
|
|
VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo();
|
|
// False (logged) when the target's surface cannot be built; the renderer then has no surface.
|
|
Bool CreateSurface();
|
|
void PickPhysicalDevice();
|
|
void CreateLogicalDeviceAndQueues();
|
|
void CreateAllocator();
|
|
void DestroyAllocator();
|
|
void CreateSwapchain();
|
|
void CreateCommandPool();
|
|
|
|
// Whether THIS draw's primitive stream restarts, and therefore what
|
|
// VkPipelineInputAssemblyStateCreateInfo::primitiveRestartEnable must be. Resolved by the
|
|
// caller because it needs two facts a pipeline cannot see: whether the draw is indexed at
|
|
// all (GL primitive restart acts on the index stream, so it is a no-op for glDrawArrays),
|
|
// and the index TYPE (an application restart index that does not fit the type matches no
|
|
// index, so that draw restarts nowhere - see UploadAndBindIndexBuffer).
|
|
Bool ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
|
|
const IndexBufferView* pIndexBufferView) const;
|
|
|
|
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
|
void DestroyComputePipelines();
|
|
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
|
// bias, line width, stencil), gated behind one render-state-parameters-version
|
|
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
|
// viewportCount is the bound pipeline's declared viewport count: 1 for every program that
|
|
// does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
|
|
// rasterizable viewport count, which takes the unmemoized array path.
|
|
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
|
|
Uint32 viewportCount = 1);
|
|
void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
|
|
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
|
|
VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
|
|
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
|
|
// How many viewports a draw with this program rasterizes into: 1 unless the program
|
|
// assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
|
|
// baked viewportCount and the dynamic arrays come from this one answer, so they cannot
|
|
// disagree.
|
|
Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
|
|
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
|
|
}
|
|
|
|
Bool InitializeBlitResources();
|
|
Bool InitializeDepthMipmapResources();
|
|
void ShutdownBlitResources();
|
|
void ShutdownDepthMipmapResources();
|
|
void CollectDeferredDepthMipmapCleanup(Uint32 frameIndex);
|
|
void DestroyDeferredDepthMipmapCleanup();
|
|
Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame);
|
|
|
|
public:
|
|
private:
|
|
|
|
void ShutdownSwapchain();
|
|
|
|
// Static functions
|
|
static Int GetPresentQueueFamilyIndex(const PhysicalDevice& physicalDevice, VkSurfaceKHR surface,
|
|
const Vector<VkQueueFamilyProperties>& queueFamilies,
|
|
Int preferredFamilyIndex = -1);
|
|
static Vector<VkQueueFamilyProperties> GetQueueFamilyFromPhysicalDevice(VkPhysicalDevice device);
|
|
static Int GetQueueFamilyIndex(const Vector<VkQueueFamilyProperties>& queueFamilies, VkQueueFlagBits flag);
|
|
static Vector<VkExtensionProperties> EnumerateInstanceExtensions();
|
|
static Vector<VkExtensionProperties> EnumerateDeviceExtensions(VkPhysicalDevice device);
|
|
static Bool IsExtensionSupported(const Vector<VkExtensionProperties>& availableExtensions,
|
|
const char* extensionName);
|
|
static Bool IsExtensionAlreadyEnabled(const Vector<const char*>& enabledExtensions, const char* extensionName);
|
|
static Bool EnableOptionalDeviceExtension(const Vector<VkExtensionProperties>& availableExtensions,
|
|
Vector<const char*>& inOutEnabledExtensions,
|
|
const char* extensionName);
|
|
void ResolveOptionalDeviceExtensions(const Vector<VkExtensionProperties>& availableExtensions,
|
|
Vector<const char*>& inOutEnabledExtensions);
|
|
static Bool IsNecessaryDeviceExtensionSupported(VkPhysicalDevice device);
|
|
static Bool GetMoreCapablePhysicalDevice(VkPhysicalDevice newVkDevice, VkSurfaceKHR surface,
|
|
const PhysicalDevice& compareWithDevice,
|
|
PhysicalDevice& outBetterDevice);
|
|
static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"};
|
|
// VK_KHR_image_format_list: lets MUTABLE_FORMAT images declare their exact view-format
|
|
// set so the driver can keep bandwidth compression (see CreateLogicalDeviceAndQueues).
|
|
Bool m_imageFormatListExtensionEnabled = false;
|
|
|
|
static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
|
|
static Bool CheckValidationLayerSupport();
|
|
|
|
static VKAPI_ATTR VkBool32 VKAPI_CALL DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
|
|
VkDebugUtilsMessageTypeFlagsEXT messageType,
|
|
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
|
|
void* pUserData);
|
|
};
|
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|