mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 13:18:31 +09:00
A destroyed VertexArrayObject's heap address is handed straight back by the
next allocation of its size, and so is a destroyed BufferObject's. DirectVulkan
keyed its per-VAO draw memo on the VAO POINTER and folded the bound buffer's
ADDRESS into the content hash that validates the memoised bindings, so a
delete/recreate pair under a byte-identical attribute layout reproduced both
the key and its validating hash at once. The successor VAO then inherited the
dead one's resolved bindings and the draw fetched from a destroyed VkBuffer.
Both stated defences failed together, because both reduce to the content hash
and the hash's buffer-identity component was itself a recycled address.
VertexArrayObject and BufferObject now carry a globally-unique, never-reused
GetLifetimeId() - the same contract as ProgramObject's, minted from an atomic
starting at 1 so a zero-initialised slot can never name a live object.
VaoDrawMemo matches on (address, lifetime id) and stores the id on recycle,
SetupDrawSnapshot's "the VAO did not move" test compares the id alongside the
config version, and VertexInputStateFactory::ComputeHash hashes the bound
buffer's id instead of its pointer (0 for client memory).
Proven: the use-after-free reproduces at 100% incidence headless on lavapipe,
including a SEGV whose backtrace is the driver dereferencing a destroyed vertex
buffer inside lvp_queue_submit, and it is gone with the fix. New coverage -
MG_Test/State/ObjectLifetimeIdTest (deterministic, GPU-free, no context: it
waits for the real allocator to repeat an address and asserts the id differs,
and skips loudly rather than passing quietly if it never gets the chance), and
MG_IntegrationTest XfbAfterClipDistanceScenario, registered for DirectGLES,
DirectVulkan, and a third DirectVulkan run with async shader compilation pinned
on because that is a second allocation pattern. Gates: 553/553 unit green at
async=0 and async=1; the scenario 5/5 headless at both flag states; 71/72 CI
trace-replay fixtures over both backends, the one failure a pre-existing
lavapipe crash proven not a regression (identical SIGSEGV at the identical
call number under the pre-fix library).
Pending NVIDIA/X11 confirmation: the KHR-GL{32,40} transform_feedback failures
that opened this investigation never reproduced on lavapipe - the -2/-101
pre-fill signature appears in zero pre-fix runs there - so whether this clears
them is UNPROVEN and must be re-measured on the NVIDIA rig against a freshly
re-run pre-fix baseline. The residual suspect is deliberately untouched here:
m_xfbCounterSlotByObject keys its counter slot on the raw GL transform-feedback
name, so a recycled name whose generation check happens to pass would RESUME
instead of BEGIN. That path was never exercised on lavapipe and is neither
confirmed nor exonerated.
130 lines
7.3 KiB
C++
130 lines
7.3 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.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 "VertexInputStateBuilder.h"
|
|
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
|
#include <Includes.h>
|
|
#include "../VkIncludes.h"
|
|
|
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
|
class VertexInputStateFactory {
|
|
public:
|
|
using HashType = Uint64;
|
|
|
|
enum class VertexStreamConversion : Uint8 {
|
|
None = 0,
|
|
Repack,
|
|
ScaledIntegerToFloat32,
|
|
};
|
|
|
|
struct BackendVertexInputState {
|
|
HashType hash = 0;
|
|
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
|
// unsupported mask) - NO buffer identities. `hash` mixes each bound
|
|
// buffer's never-reused LIFETIME ID, so per-chunk VBOs mint a fresh
|
|
// identity per buffer; keying pipelines on that minted one VkPipeline per
|
|
// chunk section for an identical layout, defeating pipeline reuse and the
|
|
// per-draw memo. Pipelines depend only on the layout, so they key on this
|
|
// instead.
|
|
HashType layoutHash = 0;
|
|
// Frame boundary of the last cache hit; entries idle past the
|
|
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
|
// Mutable: the VAO's state-pointer memo fast path stamps it through
|
|
// a const entry reference.
|
|
mutable Uint64 lastUsedFrameBoundary = 0;
|
|
Vector<VkVertexInputBindingDescription> bindings;
|
|
Vector<VkVertexInputAttributeDescription> attributes;
|
|
Vector<SizeT> bindingBufferKeys;
|
|
Vector<SizeT> bindingBaseOffsets;
|
|
Vector<Uint32> bindingAttributeLocations;
|
|
Vector<Bool> bindingUsesClientMemory;
|
|
Vector<VertexStreamConversion> bindingConversions;
|
|
// Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are
|
|
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
|
|
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
|
Uint32 unsupportedAttribMask = 0;
|
|
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
|
// three times per draw, so it is baked once at build time.
|
|
Uint32 attributeLocationMask = 0;
|
|
// Per-binding glVertexAttribDivisor values other than 1. Vulkan's instance input
|
|
// rate advances once per instance and nothing else, so anything else has to be
|
|
// stated through VK_EXT_vertex_attribute_divisor. Empty when every instanced
|
|
// binding uses divisor 1, which is what the plain input rate already means.
|
|
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
|
|
VkPipelineVertexInputDivisorStateCreateInfoEXT divisorState{
|
|
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_DIVISOR_STATE_CREATE_INFO_EXT
|
|
};
|
|
VkPipelineVertexInputStateCreateInfo state{
|
|
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
|
};
|
|
};
|
|
|
|
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
|
m_config(config), m_physicalDevice(physicalDevice) {}
|
|
~VertexInputStateFactory() = default;
|
|
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
|
|
|
// The VAO aux-memo payload GetOrCreateVertexInputState(vao) stamps: aux0 is the
|
|
// entry's layoutHash, aux1 packs (unsupportedAttribMask << 32) | attributeLocationMask.
|
|
// Readers that find the aux memo valid can use these without resolving the entry.
|
|
static Uint64 PackVertexInputAuxMasks(Uint32 unsupportedAttribMask, Uint32 attributeLocationMask) {
|
|
return (static_cast<Uint64>(unsupportedAttribMask) << 32) | attributeLocationMask;
|
|
}
|
|
|
|
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
|
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
|
// is unchanged. Use this on per-draw paths.
|
|
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
|
const BackendVertexInputState& GetOrCreateVertexInputState(
|
|
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
|
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
|
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
|
// frames. The key mixes each bound buffer's never-reused lifetime id, so
|
|
// buffer/VAO churn keeps minting fresh keys - and does so by construction,
|
|
// not by luck: a recreated buffer can no longer land back on its dead
|
|
// predecessor's key. Without eviction the map grows for the whole session.
|
|
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
|
// and the draw path's entry reference never spans a frame boundary, so
|
|
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
|
// compare except on sweep boundaries.
|
|
void OnFrameBoundary();
|
|
static SizeT GetComponentSize(DataType type);
|
|
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
|
|
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
|
// an unknown/unsupported type.
|
|
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
|
|
|
private:
|
|
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
|
|
Bool isLong = false);
|
|
static Bool IsScaledIntegerVertexFormat(VkFormat format);
|
|
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
|
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
|
|
|
const VulkanRendererConfig& m_config;
|
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
|
// Values are heap-allocated: FastSTL::unordered_map is open-addressing,
|
|
// so INSERT invalidates references to stored values. The draw path (and
|
|
// the VAOs' state-pointer memos) hold entry pointers across inserts;
|
|
// only the unique_ptr cell moves, never the pointee.
|
|
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
|
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
|
Uint64 m_frameBoundaryCounter = 0;
|
|
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
|
|
// their heap-allocated entry (stable across map insert/rehash by
|
|
// construction); a memo is honored only while its recorded epoch
|
|
// matches, so an evicted entry can never be dereferenced through a
|
|
// stale memo.
|
|
Uint64 m_evictionEpoch = 1;
|
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
|
};
|
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|