mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
A per-draw CPU profile of a real Minecraft frame (perf on the render thread, which sits at 100% of one core on both backends) said the deficit is translation overhead, not the GPU, and named where it goes. This removes the largest items it found, on both backends and in the shared frontend they both feed. The single biggest one was not translation at all: IsBackendContextCurrentOnThisThread called eglGetCurrentContext on every invocation, and glvnd answers that with a getpid() fork check - a real syscall. The predicate sits two and three deep in every draw (the deferred-release drain, the global-UBO ring availability check, and the ring allocation), so it accounted for 16.3% of the render thread. EGL is still the ground truth, but re-verifying it once per thread per frame catches an external migration at the next frame boundary rather than the next call, which recovers the same bookkeeping. Texture uploads now carry a dirty region instead of a per-level flag. Minecraft animates atlas sprites with 16x16 glTexSubImage2D calls into a 1024x512 atlas and respecifies the lightmap every frame; a per-level flag turned each of those into a full-level re-upload - about 3.6 MB a frame of texels nobody changed. MipmapStorage accumulates the written box, Espryt uploads it with UNPACK_ROW_LENGTH striding into the level shadow, and Magma stages just that box. The box is a union, not a range list: repeated writes to one level widen it and it degrades to exactly the old whole-level upload, which is the honest worst case. glBufferData(NULL) is the orphaning idiom, and the backend was answering it by uploading the stale CPU shadow - turning a rename the driver does for free into a full synchronized upload. BufferObject now records that a NULL respecify leaves the store undefined, and the upload is skipped until content is actually written. The rest are smaller and of a kind: the deferred-release queue is probed without taking its mutex, the UBO ring waits on the frame fence that frees the space it needs instead of draining the whole pipeline with glFinish at the size cap, VAO binds go through a shadow so a draw's second bind of the same object does not reach the driver, the per-draw clean-texture probe short-circuits on the content version before rebuilding shape info, glUniform drops byte-identical writes (which otherwise dirty the whole UBO for the next draw), re-binding the texture or VAO a slot already holds no longer bumps the generation counters a backend fast path is keyed on, and the texture validators stopped taking shared_ptr by value. On Magma: descriptor-set reuse keeps four entries instead of one, because draws alternating between two programs - the chunk/entity ping-pong - thrashed a single slot into a full re-allocate and re-write every draw; a DynamicDraw buffer whose contents survive two frame boundaries is promoted to resident storage instead of being re-copied into the per-frame arena forever; and sampled-read barriers name only the shader stages whose device feature is enabled, which also removes a latent VUID violation (ALL_GRAPHICS names geometry and tessellation stages a device need not have). Measured with the Minecraft rig (render distance 32, p50 fps, same machine, single sample each): vanilla 1.21.1 Espryt 10.8 -> 36.3 and Magma 31.3 -> 44.6; 26.2 snapshot Magma 114.5 -> 210.5. Fabric+Sodium moved inside noise on Magma (854 -> 766) with the native baseline itself moving 838 -> 1031 between the two sessions, so treat that cell as unresolved rather than a regression measured. Unit tests 421/421. The CTS A/B was not run: these numbers and the test suite are the whole of the evidence, and a conformance regression would not have been caught here.
522 lines
28 KiB
C++
522 lines
28 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.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 "../VkIncludes.h"
|
|
#include <Includes.h>
|
|
#include <MG_State/GLState/TextureState/TextureObject.h>
|
|
#include <vk_mem_alloc.h>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
|
|
namespace MobileGL::MG_State::GLState {
|
|
class ITextureObject;
|
|
}
|
|
|
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
|
enum class SamplerNumericDomain : Uint8;
|
|
|
|
class VkTextureManager {
|
|
public:
|
|
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
|
// manager keys its per-draw fast path on this so an attachment's image recreation
|
|
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
|
|
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
|
|
// Bumped whenever any tracked texture resource is erased; cached
|
|
// TextureResource pointers are valid only while this is unchanged.
|
|
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
|
|
|
|
struct TextureIdentity {
|
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
|
Uint64 lifetimeId = 0;
|
|
|
|
Bool operator==(const TextureIdentity& other) const {
|
|
return texture == other.texture && lifetimeId == other.lifetimeId;
|
|
}
|
|
};
|
|
|
|
struct TextureIdentityHash {
|
|
SizeT operator()(const TextureIdentity& key) const {
|
|
SizeT hash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
|
|
hash ^= std::hash<Uint64>{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
return hash;
|
|
}
|
|
};
|
|
|
|
struct InitInfo {
|
|
VkDevice device = VK_NULL_HANDLE;
|
|
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
|
|
VmaAllocator allocator = nullptr;
|
|
VkCommandPool commandPool = VK_NULL_HANDLE;
|
|
VkQueue graphicsQueue = VK_NULL_HANDLE;
|
|
Uint32 frameCount = 0;
|
|
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
|
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
|
Bool imageFormatListSupported = false;
|
|
// Union of shader stages sampled-read barriers may name on this device; the renderer
|
|
// builds it from the enabled features because geometry/tessellation stage bits are
|
|
// invalid in a barrier when their feature is off.
|
|
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
|
};
|
|
|
|
struct TextureResource {
|
|
struct AttachmentViewKey {
|
|
Uint32 mipLevel = 0;
|
|
Uint32 baseArrayLayer = 0;
|
|
Uint32 layerCount = 1;
|
|
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
// May differ from the image format: sRGB images attach through their UNORM
|
|
// twin while GL_FRAMEBUFFER_SRGB is disabled.
|
|
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
|
|
|
Bool operator==(const AttachmentViewKey& other) const {
|
|
return mipLevel == other.mipLevel &&
|
|
baseArrayLayer == other.baseArrayLayer &&
|
|
layerCount == other.layerCount &&
|
|
viewType == other.viewType &&
|
|
viewFormat == other.viewFormat;
|
|
}
|
|
};
|
|
|
|
struct AttachmentViewKeyHash {
|
|
SizeT operator()(const AttachmentViewKey& key) const {
|
|
SizeT hash = std::hash<Uint32>{}(key.mipLevel);
|
|
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewFormat)) +
|
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
return hash;
|
|
}
|
|
};
|
|
|
|
struct StorageImageViewKey {
|
|
Uint32 mipLevel = 0;
|
|
Uint32 baseArrayLayer = 0;
|
|
Uint32 layerCount = 1;
|
|
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
|
|
Bool operator==(const StorageImageViewKey& other) const {
|
|
return mipLevel == other.mipLevel &&
|
|
baseArrayLayer == other.baseArrayLayer &&
|
|
layerCount == other.layerCount &&
|
|
viewType == other.viewType &&
|
|
format == other.format;
|
|
}
|
|
};
|
|
|
|
struct SampledImageViewKey {
|
|
Uint32 baseMipLevel = 0;
|
|
Uint32 levelCount = 1;
|
|
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
|
|
Bool operator==(const SampledImageViewKey& other) const {
|
|
return baseMipLevel == other.baseMipLevel &&
|
|
levelCount == other.levelCount &&
|
|
viewType == other.viewType &&
|
|
format == other.format;
|
|
}
|
|
};
|
|
|
|
struct SampledImageViewKeyHash {
|
|
SizeT operator()(const SampledImageViewKey& key) const {
|
|
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
|
|
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
|
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
return hash;
|
|
}
|
|
};
|
|
|
|
struct StorageImageViewKeyHash {
|
|
SizeT operator()(const StorageImageViewKey& key) const {
|
|
SizeT hash = std::hash<Uint32>{}(key.mipLevel);
|
|
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
|
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
|
return hash;
|
|
}
|
|
};
|
|
|
|
VkImage image = VK_NULL_HANDLE;
|
|
VmaAllocation allocation = nullptr;
|
|
VkImageView fullView = VK_NULL_HANDLE;
|
|
VkImageView sampledView = VK_NULL_HANDLE;
|
|
Vector<VkImageView> perMipViews;
|
|
Vector<VkImageView> perMipSampledViews;
|
|
UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews;
|
|
UnorderedMap<SampledImageViewKey, VkImageView, SampledImageViewKeyHash> alternateSampledViews;
|
|
UnorderedMap<StorageImageViewKey, VkImageView, StorageImageViewKeyHash> storageImageViews;
|
|
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
VkExtent2D extent = {0, 0};
|
|
Uint32 depth = 1;
|
|
Uint32 arrayLayers = 1;
|
|
Uint32 mipLevels = 1;
|
|
Uint32 sampledBaseMipLevel = 0;
|
|
Uint32 sampledLevelCount = 1;
|
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
|
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
VkImageCreateFlags imageCreateFlags = 0;
|
|
// Usage the live image was created with. STORAGE is only requested for textures that
|
|
// have actually been bound to a GL image unit, because on Adreno a storage-capable
|
|
// image loses UBWC bandwidth compression; a later image binding upgrades the usage
|
|
// and recreates the image, so the resolved usage has to be part of the compatibility
|
|
// check that decides whether the existing image can be kept.
|
|
VkImageUsageFlags usageFlags = 0;
|
|
// True once this image was (re)resolved while the texture was already marked as an
|
|
// image-unit texture. Distinguishes "not upgraded yet" from "cannot be upgraded"
|
|
// (a format whose optimalTilingFeatures lack STORAGE_IMAGE never gains the bit), so
|
|
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
|
|
Bool storageUsageResolved = false;
|
|
Uint16 syncedTextureParamsVersion = 0;
|
|
// Recording generation (VkTextureManager::GetRecordingGeneration) of the last
|
|
// command referencing this image that was recorded into the CURRENT frame
|
|
// command buffer. An image untouched by the open recording may have its
|
|
// out-of-pass work (deferred clears, sampled-layout transitions) recorded
|
|
// into the frame's PRE command buffer - which executes strictly before the
|
|
// frame's commands - instead of splitting the active render pass.
|
|
Uint64 lastRecordingGeneration = 0;
|
|
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
|
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
|
Uint64 syncedContentVersion = 0;
|
|
// Snapshot of the defined mip-level count at the last sync. Folded into the early-out key
|
|
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
|
|
// busts the skip even if it failed to bump the content version.
|
|
Uint32 syncedMipLevelCount = 0;
|
|
|
|
TextureResource() = default;
|
|
TextureResource(const TextureResource&) = delete;
|
|
TextureResource(TextureResource&& that) noexcept {
|
|
std::swap(this->image, that.image);
|
|
std::swap(this->allocation, that.allocation);
|
|
std::swap(this->fullView, that.fullView);
|
|
std::swap(this->sampledView, that.sampledView);
|
|
std::swap(this->perMipViews, that.perMipViews);
|
|
std::swap(this->perMipSampledViews, that.perMipSampledViews);
|
|
std::swap(this->attachmentViews, that.attachmentViews);
|
|
std::swap(this->alternateSampledViews, that.alternateSampledViews);
|
|
std::swap(this->storageImageViews, that.storageImageViews);
|
|
std::swap(this->layout, that.layout);
|
|
std::swap(this->extent, that.extent);
|
|
std::swap(this->depth, that.depth);
|
|
std::swap(this->arrayLayers, that.arrayLayers);
|
|
std::swap(this->mipLevels, that.mipLevels);
|
|
std::swap(this->sampledBaseMipLevel, that.sampledBaseMipLevel);
|
|
std::swap(this->sampledLevelCount, that.sampledLevelCount);
|
|
std::swap(this->format, that.format);
|
|
std::swap(this->aspect, that.aspect);
|
|
std::swap(this->viewType, that.viewType);
|
|
std::swap(this->sampleCount, that.sampleCount);
|
|
std::swap(this->imageCreateFlags, that.imageCreateFlags);
|
|
std::swap(this->usageFlags, that.usageFlags);
|
|
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
|
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
|
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
|
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
|
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
|
}
|
|
|
|
void Reset() {
|
|
if (fullView != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(s_device, fullView, nullptr);
|
|
}
|
|
if (sampledView != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(s_device, sampledView, nullptr);
|
|
}
|
|
for (const auto attachmentView : perMipViews) {
|
|
if (attachmentView != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(s_device, attachmentView, nullptr);
|
|
}
|
|
}
|
|
for (const auto sampledView : perMipSampledViews) {
|
|
if (sampledView != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(s_device, sampledView, nullptr);
|
|
}
|
|
}
|
|
for (const auto& [_, attachmentView] : attachmentViews) {
|
|
if (attachmentView != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(s_device, attachmentView, nullptr);
|
|
}
|
|
}
|
|
for (const auto& [_, sampledView] : alternateSampledViews) {
|
|
if (sampledView != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(s_device, sampledView, nullptr);
|
|
}
|
|
}
|
|
for (const auto& [_, storageImageView] : storageImageViews) {
|
|
if (storageImageView != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(s_device, storageImageView, nullptr);
|
|
}
|
|
}
|
|
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
|
vmaDestroyImage(s_allocator, image, allocation);
|
|
}
|
|
fullView = VK_NULL_HANDLE;
|
|
sampledView = VK_NULL_HANDLE;
|
|
perMipViews.clear();
|
|
perMipSampledViews.clear();
|
|
attachmentViews.clear();
|
|
alternateSampledViews.clear();
|
|
storageImageViews.clear();
|
|
image = VK_NULL_HANDLE;
|
|
allocation = nullptr;
|
|
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
extent = {0, 0};
|
|
depth = 1;
|
|
arrayLayers = 1;
|
|
mipLevels = 1;
|
|
sampledBaseMipLevel = 0;
|
|
sampledLevelCount = 1;
|
|
format = VK_FORMAT_UNDEFINED;
|
|
aspect = VK_IMAGE_ASPECT_NONE;
|
|
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
imageCreateFlags = 0;
|
|
usageFlags = 0;
|
|
storageUsageResolved = false;
|
|
syncedTextureParamsVersion = 0;
|
|
syncedContentVersion = 0;
|
|
syncedMipLevelCount = 0;
|
|
}
|
|
|
|
~TextureResource() {
|
|
Reset();
|
|
}
|
|
|
|
static inline VkDevice s_device = VK_NULL_HANDLE;
|
|
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
|
|
};
|
|
|
|
Bool Initialize(const InitInfo& initInfo);
|
|
void Shutdown();
|
|
void BeginFrame(Uint32 frameIndex);
|
|
// Drains every frame slot's deferred image/view releases. Only valid when
|
|
// the caller has proven every queue submission complete; used by the
|
|
// present-less frame-boundary drain.
|
|
void CollectAllDeferredReleases();
|
|
|
|
TextureResource* SyncTextureAndGetDescriptor(
|
|
MG_State::GLState::ITextureObject& texture);
|
|
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
|
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
|
Uint32 baseArrayLayer, Uint32 layerCount,
|
|
VkImageViewType viewType);
|
|
VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
|
VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format);
|
|
VkImageView GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
|
VkFormat format, Bool layered, Int32 layer);
|
|
void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout);
|
|
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
|
|
MG_State::GLState::ITextureObject* texture,
|
|
Uint32 writtenMipLevel,
|
|
VkImageLayout newLayout);
|
|
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
|
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
|
|
|
// Recording-generation bookkeeping for the pre-pass command stream. The
|
|
// generation advances every time the frame command buffer (re)begins
|
|
// recording; a resource whose stamp does not match was not referenced by
|
|
// any command in the open recording, so its out-of-pass work may safely
|
|
// execute ahead of the whole recording (in the pre command buffer).
|
|
void AdvanceRecordingGeneration() { ++m_recordingGeneration; }
|
|
void StampResourceRecordingUse(TextureResource& resource) const {
|
|
resource.lastRecordingGeneration = m_recordingGeneration;
|
|
}
|
|
// Map-lookup variant for callers that only hold the GL texture object.
|
|
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
|
|
Bool WasTouchedThisRecording(const TextureResource& resource) const {
|
|
return resource.lastRecordingGeneration == m_recordingGeneration;
|
|
}
|
|
// Records that this texture is bound to a GL image unit, so its image must carry
|
|
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
|
// therefore before the render pass is committed: an image that has to be upgraded is
|
|
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
|
|
// GL lets an image binding come and go, and re-creating the image every time it does
|
|
// would cost far more than the compression it wins back.
|
|
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
|
|
// True when this texture is marked but its live image predates the mark, i.e. the next sync
|
|
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
|
// submit their pending recording first, so that copy cannot read pre-flush content.
|
|
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
|
// The same ordering question for the other recreate-and-preserve trigger: true when this
|
|
// texture's live image carries a shorter mip chain than a full one, so defining the missing
|
|
// levels recreates it and copies the old contents forward.
|
|
Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const;
|
|
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
|
// texture as a storage image may need work that is illegal inside a render pass (resource
|
|
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
|
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
|
|
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
|
|
|
|
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
|
|
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
|
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
|
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
|
|
|
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
|
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
|
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
|
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
|
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
|
|
Uint32 layerCount = 1);
|
|
|
|
SizeT CollectGarbage();
|
|
|
|
// Per-draw sync memo. Within a single SetupDraw the same sampled texture is
|
|
// resolved ~3x (SetupDraw's layout-probe loop, its post-transition loop, and
|
|
// again inside ResolveSamplerDescriptor). No GL texture mutation can happen
|
|
// mid-SetupDraw, and layout is tracked on the TextureResource independently of
|
|
// SyncTexture, so after the first successful sync of a texture in a draw the
|
|
// heavy SyncTexture work (mip-completeness/resource/view resync + dirty scan)
|
|
// is pure redundancy. BeginDrawSyncScope opens a window in which repeat
|
|
// SyncTextureAndGetDescriptor calls short-circuit to the already-synced
|
|
// resource; EndDrawSyncScope closes it. Use the RAII DrawSyncScope guard.
|
|
void BeginDrawSyncScope();
|
|
void EndDrawSyncScope();
|
|
|
|
// RAII guard that opens/closes a per-draw sync memo window (see above).
|
|
class DrawSyncScope {
|
|
public:
|
|
explicit DrawSyncScope(VkTextureManager& manager) : m_manager(manager) { m_manager.BeginDrawSyncScope(); }
|
|
~DrawSyncScope() { m_manager.EndDrawSyncScope(); }
|
|
DrawSyncScope(const DrawSyncScope&) = delete;
|
|
DrawSyncScope& operator=(const DrawSyncScope&) = delete;
|
|
private:
|
|
VkTextureManager& m_manager;
|
|
};
|
|
|
|
private:
|
|
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
|
|
Uint64 m_textureImageEpoch = 1;
|
|
// See AdvanceRecordingGeneration. Starts above every resource's default
|
|
// stamp of 0 so a fresh resource counts as untouched.
|
|
Uint64 m_recordingGeneration = 1;
|
|
|
|
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
|
|
TextureResource &outResource);
|
|
Bool SyncTextureResource(const MG_State::GLState::ITextureObject &texture,
|
|
TextureUploadTarget uploadTarget,
|
|
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
|
|
TextureResource &resource);
|
|
Bool SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource);
|
|
VkImageView CreateImageView(VkImage image, VkFormat format, VkImageAspectFlags aspect,
|
|
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
|
|
Uint32 baseArrayLayer,
|
|
Uint32 layerCount,
|
|
const VkComponentMapping* components = nullptr,
|
|
VkImageUsageFlags viewUsage = 0) const;
|
|
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
|
|
TextureUploadTarget uploadTarget,
|
|
TextureResource &outResource);
|
|
static Bool CheckMipmapCompleteness(const MG_State::GLState::ITextureObject& texture,
|
|
TextureUploadTarget& outTarget,
|
|
IntVec3& outTexelSize,
|
|
SizeT& outByteSize,
|
|
Uint32& outMipLevelCount);
|
|
static Uint32 GetUploadMipLevelCount(const MG_State::GLState::TextureObjectMipmap& texture, TextureUploadTarget target);
|
|
static void ResolveViewMipRange(const MG_State::GLState::ITextureObject& texture, Uint32 mipLevels,
|
|
Uint32& outBaseMipLevel, Uint32& outLevelCount);
|
|
static VkImageAspectFlags GetAspectMaskForFormat(VkFormat format);
|
|
void DeferResourceRelease(TextureResource&& resource);
|
|
void DeferViewRelease(VkImageView view);
|
|
void CollectDeferredReleases(Uint32 frameIndex);
|
|
void DestroyDeferredReleases();
|
|
// Frees the fence/command buffer/staging buffer of every in-flight texture
|
|
// upload whose fence has signaled (submission order = completion order on
|
|
// the single queue, so the scan stops at the first still-pending entry).
|
|
// waitAll blocks on every entry - Shutdown's drain.
|
|
void ReclaimCompletedUploads(Bool waitAll = false);
|
|
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
|
void EraseTrackedTexture(const TextureIdentity& identity);
|
|
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
|
SizeT PruneDeadTextures();
|
|
|
|
VkDevice m_device = VK_NULL_HANDLE;
|
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
|
VmaAllocator m_allocator = nullptr;
|
|
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
|
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
|
Bool m_imageFormatListSupported = false;
|
|
Uint32 m_currentFrameIndex = 0;
|
|
|
|
Uint8 m_gcCounter = 0;
|
|
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
|
|
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
|
|
Uint32 m_gcFrameCounter = 0;
|
|
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
|
// textures already fully synced in the current draw (small N -> flat scan).
|
|
Bool m_drawSyncScopeActive = false;
|
|
// Per-draw sync memo: the identity plus the resolved resource pointer. The pointer is stable
|
|
// across rehash in the node-based m_textureResources and stays valid for the draw (a texture
|
|
// synced this draw is alive and is not erased mid-draw), so a repeat sync of the same texture
|
|
// returns the resource without re-hashing the identity into m_textureResources.
|
|
struct DrawSyncedTexture {
|
|
TextureIdentity identity;
|
|
TextureResource* resource = nullptr;
|
|
};
|
|
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
|
|
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
|
|
// are resolved on every draw, so cache their resource pointers and skip the
|
|
// alive/resource map lookups. Node-based std::unordered_map keeps the
|
|
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
|
|
// every memo entry must match. SyncTexture still runs on memo hits, so
|
|
// content/param freshness is unaffected. A dead-then-reused texture address
|
|
// cannot false-hit: the new object carries a new lifetime id.
|
|
struct SyncedTextureMemoEntry {
|
|
const MG_State::GLState::ITextureObject* texture = nullptr;
|
|
Uint64 lifetimeId = 0;
|
|
Uint64 eraseEpoch = 0;
|
|
TextureResource* resource = nullptr;
|
|
};
|
|
static constexpr Uint32 kSyncedTextureMemoSize = 8;
|
|
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
|
|
Uint32 m_syncedTextureMemoNext = 0;
|
|
Uint64 m_resourceEraseEpoch = 1;
|
|
// Formats whose mutable-image probe failed on this device; their images are created
|
|
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
|
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
|
// Formats whose 3D images refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT. Per format+usage,
|
|
// exactly like the mutable-format verdict above, so it is answered at image creation and
|
|
// remembered rather than probed once globally.
|
|
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
|
|
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
|
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
|
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
|
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
|
// Supported multisample counts per format, so repeat texture syncs do not
|
|
// re-query vkGetPhysicalDeviceImageFormatProperties.
|
|
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
|
Vector<Vector<TextureResource>> m_deferredReleases;
|
|
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
|
// Texture uploads are submitted out-of-band but NOT waited on (waiting
|
|
// behind the queue serialized the CPU against the previous frame's GPU
|
|
// work every time an animated atlas re-uploaded). Their transient objects
|
|
// are parked here and reclaimed once the upload fence signals.
|
|
struct PendingUploadReclaim {
|
|
VkFence fence = VK_NULL_HANDLE;
|
|
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
|
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
|
VmaAllocation stagingAllocation = nullptr;
|
|
};
|
|
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
|
|
};
|
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|