mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Refactor] (P13 W6c): Magma's frontend arm goes - the frontend program source, sampled-set walk, placeholder textures, VAO draw memos, identity tables and fast path - leaving the record arm.
This commit is contained in:
@@ -399,18 +399,6 @@ namespace MobileGL::MG_Config {
|
||||
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipePoisonOmit;
|
||||
// MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): replace the
|
||||
// OBJECT IDENTITY in every DirectVulkan vertex-input memo key with a constant, on
|
||||
// whichever arm the run is on - the pre-handle (address, lifetime id) pair AND the
|
||||
// handle arm's {slot, gen} generation - so a replacement object inherits its dead
|
||||
// predecessor's resolved vertex bindings and HandleRecycleScenario.AbaControl asserts
|
||||
// the WRONG pixels. That is what proves the reproducer still reproduces. D18 wrote
|
||||
// this as "hash the raw BufferObject* instead of its lifetime id"; measured, the heap
|
||||
// block is never handed back, so that spelling collided with nothing and the control
|
||||
// went vacuous - see MagmaPipeArms.h's MagmaPipeAbaControlDefeatsIdentity for the
|
||||
// measurement and for what the control still leaves standing. Under
|
||||
// MOBILEGL_PIPE_PUSH only, so it cannot exist in a shipping pull build.
|
||||
Bool PipeHandleAbaControl = false;
|
||||
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
|
||||
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
|
||||
Bool PipeStats = false;
|
||||
|
||||
@@ -412,7 +412,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
|
||||
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
|
||||
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
|
||||
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
|
||||
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
|
||||
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
|
||||
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
|
||||
|
||||
@@ -2236,50 +2236,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const SharedPtr<MG_State::GLState::BufferObject>* ElementBuffer = nullptr;
|
||||
};
|
||||
|
||||
static Bool ReadClientSnapshotBytes(void* user, MG_Pipe::MGPipeClientBufferKind kind, Uint64 offset, SizeT size,
|
||||
void* destination) {
|
||||
if (kind != MG_Pipe::MGPipeClientBufferKind::Element) return false;
|
||||
const auto& buffer = *static_cast<const ClientSnapshotSources*>(user)->ElementBuffer;
|
||||
if (!buffer || offset > buffer->GetSize() || size > buffer->GetSize() - offset) return false;
|
||||
// A shader-written index stream is exactly the case this snapshot exists for, so the
|
||||
// object's shadow is reconciled before it is read (SyncGpuWrites is a no-op otherwise).
|
||||
buffer->SyncGpuWrites();
|
||||
buffer->DownloadSubData(destination, static_cast<SizeT>(offset), size);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The draw's own fetch, spelled as the range record spells it: `indexSize` 0 is arrays,
|
||||
// `elementStart` is the first element (an element offset into the bound element buffer, or
|
||||
// 0 for the application's own index array), and `clientIndices` names that array when
|
||||
// there is one. FALSE means the caller must SKIP the draw: the elements it would fetch are
|
||||
// not in the store the driver is about to read, and issuing it anyway is a wrong picture
|
||||
// rather than an error.
|
||||
// Does the VAO feed any enabled attribute from client memory? `vertexRate`, when given,
|
||||
// is set when one of those attributes advances per vertex rather than per instance.
|
||||
static Bool VaoHasEnabledClientArray(const MG_State::GLState::VertexArrayObject& vao,
|
||||
Bool* vertexRate = nullptr) {
|
||||
Bool any = false;
|
||||
const auto& attributes = vao.GetAllAttributes();
|
||||
for (SizeT i = 0; i < attributes.size(); ++i) {
|
||||
if (!attributes[i].Enabled || attributes[i].Buffer) continue;
|
||||
any = true;
|
||||
if (vertexRate != nullptr) *vertexRate |= attributes[i].Divisor == 0;
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
static Bool SyncClientSideVertexArraysForFetch(Uint8 indexSize, Uint32 elementStart, GLsizei count,
|
||||
GLsizei instanceCount, GLint baseVertex, Uint32 baseInstance,
|
||||
const void* clientIndices, Uint64 clientIndexBytes) {
|
||||
if (count <= 0 || instanceCount <= 0) return true;
|
||||
// MONOLITH ONLY, and the gate is what keeps a live transport's apply thread out of
|
||||
// the frontend VAO: with a transport the CLIENT owns those bytes and snapshots them
|
||||
// from the GL thread (MG_Impl/Pipe/OwnedDrawInputs.h), so there is nothing for the
|
||||
// server to stage - and reading gPipeInputs there is the role violation this family's
|
||||
// other monolith glue is guarded against.
|
||||
return true;
|
||||
}
|
||||
|
||||
// The indirect executors' form: the command's own words ARE the draw's fetch, and a
|
||||
// command block a shader wrote this frame is exactly what these shapes exist for - so the
|
||||
// command buffer's shadow is reconciled before it is read, and `commandBytes` (the
|
||||
@@ -10623,66 +10579,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
static Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize) {
|
||||
Int maxDimension = std::max<Int>(
|
||||
baseTexelSize.x(),
|
||||
std::max<Int>(baseTexelSize.y(), std::max<Int>(baseTexelSize.z(), 1)));
|
||||
Uint mipLevelCount = 1;
|
||||
while (maxDimension > 1) {
|
||||
maxDimension = std::max<Int>(maxDimension / 2, 1);
|
||||
++mipLevelCount;
|
||||
}
|
||||
return mipLevelCount;
|
||||
}
|
||||
|
||||
static IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel) {
|
||||
return {
|
||||
std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1),
|
||||
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1),
|
||||
std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1),
|
||||
};
|
||||
}
|
||||
|
||||
static Bool EnsureGenerateMipmapStorageAllocated(MG_State::GLState::TextureObjectMipmap& texture,
|
||||
TextureUploadTarget uploadTarget, Bool& allocatedStorage) {
|
||||
const Uint existingLevelCount = texture.GetMipmapLevelCount();
|
||||
if (existingLevelCount == 0) {
|
||||
return false;
|
||||
}
|
||||
// P4a (D-M). glGenerateMipmap's storage grow reaches into the frontend texture's own
|
||||
// level shadows to decide - and then to define - the levels the driver is about to
|
||||
// fill. In monolith that is exactly what it does today and nothing here changes; under
|
||||
// a split there is no client address space to reach into, so P8 gives this name teeth.
|
||||
// Named and greppable rather than silent, so the site cannot quietly disappear before
|
||||
// then.
|
||||
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
|
||||
|
||||
const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, 0);
|
||||
const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, 0);
|
||||
const SizeT baseTexelCount = static_cast<SizeT>(baseTexelSize.x()) *
|
||||
static_cast<SizeT>(baseTexelSize.y()) *
|
||||
static_cast<SizeT>(baseTexelSize.z());
|
||||
if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0 ||
|
||||
baseByteSize == 0 || baseTexelCount == 0 || (baseByteSize % baseTexelCount) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
|
||||
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize);
|
||||
if (existingLevelCount < requiredLevelCount) {
|
||||
allocatedStorage = true;
|
||||
}
|
||||
for (Uint level = existingLevelCount; level < requiredLevelCount; ++level) {
|
||||
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level);
|
||||
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
|
||||
static_cast<SizeT>(levelTexelSize.y()) *
|
||||
static_cast<SizeT>(levelTexelSize.z());
|
||||
texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize});
|
||||
texture.MarkStorageDirty(uploadTarget, level, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// P5e (tx2): the inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the two
|
||||
// descriptor reads this file makes (Managers.cpp has the same inverse beside the storage
|
||||
// sync; it is file-local there, and a header for eleven cases would be the wrong trade).
|
||||
@@ -10740,21 +10636,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return window;
|
||||
}
|
||||
|
||||
static Bool EnsureGenerateMipmapStorageAllocated(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
||||
// P8-B1: the transport arm that stood here (a descriptor check under the same
|
||||
// `generate-mipmap-storage` name) was unreachable - GenerateMipmap returns through
|
||||
// GenerateMipmapByRecord before this call on every split arm - and the record arm's
|
||||
// window above replaces the check it made.
|
||||
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
|
||||
MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires mipmap texture storage.");
|
||||
Bool allocatedStorage = false;
|
||||
for (const TextureUploadTarget uploadTarget : texture->GetUploadTargets()) {
|
||||
const Bool allocated = EnsureGenerateMipmapStorageAllocated(*mipmapTexture, uploadTarget, allocatedStorage);
|
||||
MOBILEGL_ASSERT(allocated, "GenerateMipmap could not allocate generated mipmap storage.");
|
||||
}
|
||||
return allocatedStorage;
|
||||
}
|
||||
|
||||
static void AssertNoGLError(const char* operation) {
|
||||
const GLenum err = g_GLESFuncs.glGetError();
|
||||
MOBILEGL_ASSERT(err == GL_NO_ERROR, "%s failed: %s", operation,
|
||||
@@ -11444,137 +11325,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
static void GenerateDepthTexture2DMipmap(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& texture,
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
|
||||
MOBILEGL_ASSERT(texture != nullptr && backendTexture != nullptr, "GenerateDepthTexture2DMipmap needs texture.");
|
||||
MOBILEGL_ASSERT(texture->GetTarget() == TextureTarget::Texture2D,
|
||||
"DirectGLES depth mipmap generation only supports GL_TEXTURE_2D.");
|
||||
MOBILEGL_ASSERT(IsDepthOnlyFormat(texture->GetFormat()),
|
||||
"DirectGLES depth mipmap generation requires a depth-only texture.");
|
||||
|
||||
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
|
||||
MOBILEGL_ASSERT(mipmapTexture != nullptr, "Depth mipmap generation requires mipmap storage.");
|
||||
|
||||
// P5e (tx2): the transport arm that stood here - a scoped `HandleOf(texture.get())`
|
||||
// probe to re-derive a handle the generate_mipmap record already carried (CONTRACT-P5E
|
||||
// §4.4's site 9161) - is GenerateDepthTexture2DMipmapByRecord above, reached from
|
||||
// GenerateMipmapByRecord with the handle passed in. This overload is the monolith body.
|
||||
|
||||
const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount();
|
||||
MOBILEGL_ASSERT(mipLevelCount > 0, "Depth mipmap generation requires allocated storage.");
|
||||
|
||||
const GLuint textureId = backendTexture->GetBackendTextureId();
|
||||
for (Uint level = 1; level < mipLevelCount; ++level) {
|
||||
const IntVec3 srcSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, level - 1);
|
||||
const IntVec3 dstSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, level);
|
||||
BlitDepthTexture2D(textureId, static_cast<GLint>(level - 1), 0, 0,
|
||||
static_cast<GLsizei>(srcSize.x()), static_cast<GLsizei>(srcSize.y()),
|
||||
textureId, static_cast<GLint>(level), 0, 0,
|
||||
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()));
|
||||
}
|
||||
}
|
||||
|
||||
static void GenerateColorTexture2DMipmap(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& texture,
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
|
||||
MOBILEGL_ASSERT(texture != nullptr && backendTexture != nullptr, "GenerateColorTexture2DMipmap needs texture.");
|
||||
MOBILEGL_ASSERT(texture->GetTarget() == TextureTarget::Texture2D,
|
||||
"DirectGLES color mipmap generation only supports GL_TEXTURE_2D.");
|
||||
MOBILEGL_ASSERT(IsColorOnlyFormat(texture->GetFormat()),
|
||||
"DirectGLES color mipmap generation requires a color-only texture.");
|
||||
|
||||
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
|
||||
MOBILEGL_ASSERT(mipmapTexture != nullptr, "Color mipmap generation requires mipmap storage.");
|
||||
|
||||
// P5e (tx2): the transport arm that stood here is GenerateColorTexture2DMipmapByRecord
|
||||
// above (CONTRACT-P5E §4.4's site 9216); this overload is the monolith body.
|
||||
|
||||
const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount();
|
||||
MOBILEGL_ASSERT(mipLevelCount > 0, "Color mipmap generation requires allocated storage.");
|
||||
|
||||
const GLenum filter = IsIntegerColorFormat(texture->GetFormat()) ? GL_NEAREST : GL_LINEAR;
|
||||
const GLuint textureId = backendTexture->GetBackendTextureId();
|
||||
for (Uint level = 1; level < mipLevelCount; ++level) {
|
||||
const IntVec3 srcSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, level - 1);
|
||||
const IntVec3 dstSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, level);
|
||||
BlitColorTexture2D(textureId, static_cast<GLint>(level - 1), 0, 0,
|
||||
static_cast<GLsizei>(srcSize.x()), static_cast<GLsizei>(srcSize.y()),
|
||||
textureId, static_cast<GLint>(level), 0, 0,
|
||||
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()), filter);
|
||||
}
|
||||
}
|
||||
|
||||
// Where a target keeps its LAYER count, the state-side twin of the wire descriptor's
|
||||
// ArrayLayers: a 1D array's layers live in the state-side height, every other layered target
|
||||
// keeps them in z, a cube map has six faces, and everything else has one.
|
||||
static Uint TextureLayerCount(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
||||
const IntVec3 base = texture->GetBaseSize();
|
||||
switch (texture->GetTarget()) {
|
||||
case TextureTarget::Texture1DArray:
|
||||
return static_cast<Uint>(std::max<Int>(base.y(), 1));
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return static_cast<Uint>(std::max<Int>(base.z(), 1));
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return 6;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The monolith twin of the record arm's view-window rule. Same window, read off the frontend
|
||||
// objects this arm still has: the storage owner is what the view's texels belong to, and the
|
||||
// view's own level range and layer window are stated relative to it.
|
||||
static Bool GenerateMipmapThroughViewWindowForTexture(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
||||
MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmapThroughViewWindowForTexture needs a texture.");
|
||||
if (!texture->IsTextureView()) return false;
|
||||
const auto& owner = texture->GetViewStorageOwner();
|
||||
if (owner == nullptr) return false;
|
||||
if (texture->GetSamples() != 0) return false;
|
||||
const Bool depth = IsDepthOnlyFormat(texture->GetFormat());
|
||||
if (!depth && !IsColorOnlyFormat(texture->GetFormat())) return false;
|
||||
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
|
||||
if (mipmapTexture == nullptr || texture->GetUploadTargets().empty()) return false;
|
||||
|
||||
const Uint minLayer = texture->GetViewMinLayer();
|
||||
const Uint numLayers = texture->GetViewNumLayers();
|
||||
if (numLayers == 0) return false;
|
||||
// A window that covers the whole storage is what the native call already does.
|
||||
if (minLayer == 0 && numLayers >= TextureLayerCount(owner)) return false;
|
||||
|
||||
MipmapViewWindow window;
|
||||
window.MinLevel = texture->GetViewMinLevel();
|
||||
window.MinLayer = minLayer;
|
||||
window.NumLayers = numLayers;
|
||||
const auto levelRange = texture->GetLevelRange();
|
||||
// The view's chain ends at the first of: its MAX_LEVEL, its window's level count, and
|
||||
// the levels its storage actually has.
|
||||
window.LogicalBase = levelRange.x();
|
||||
window.LogicalEnd = std::min({static_cast<Uint32>(levelRange.y()) + 1,
|
||||
static_cast<Uint32>(texture->GetViewNumLevels()),
|
||||
static_cast<Uint32>(mipmapTexture->GetMipmapLevelCount())});
|
||||
window.Depth = depth;
|
||||
window.Filter = IsIntegerColorFormat(texture->GetFormat()) ? GL_NEAREST : GL_LINEAR;
|
||||
if (window.LogicalEnd <= window.LogicalBase + 1) return true;
|
||||
|
||||
// The STORAGE's ES name is what the blits write: the view's own name aliases the same
|
||||
// texels, but only the owner's name carries every level and layer this window reaches.
|
||||
auto& storage = TextureImpl::SyncTextureObjectToBackend(owner);
|
||||
if (!storage || storage->GetBackendTextureId() == 0) {
|
||||
MGLOG_E_ONCE("DirectGLES: generate_mipmap through texture view %u has no storage texture to write; "
|
||||
"the call is dropped", texture->GetExternalIndex());
|
||||
return true;
|
||||
}
|
||||
window.StorageTextureId = storage->GetBackendTextureId();
|
||||
const TextureUploadTarget uploadTarget = texture->GetUploadTargets()[0];
|
||||
GenerateMipmapThroughViewWindow(window, [mipmapTexture, uploadTarget](Uint32 level) {
|
||||
return mipmapTexture->GetMipmapTexelSize(uploadTarget, level);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border) {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
@@ -11770,84 +11520,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
// ES has no colour-renderable three-channel float format, and its glGenerateMipmap
|
||||
// requires one, so it rejects GL_RGB16F and GL_RGB32F outright where every desktop
|
||||
// driver accepts them. Both store a plain array of floats, and a format the driver
|
||||
// cannot render into is a format nothing can have rendered into - so the frontend's own
|
||||
// copy of the texels is the authority, and the chain can be filtered there and carried
|
||||
// down by the ordinary upload path. Returns false for anything else, leaving the
|
||||
// driver's answer (including its error) in place.
|
||||
static Bool GenerateThreeChannelFloatMipmapOnCpu(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
||||
if (!texture) return false;
|
||||
const TextureInternalFormat format = texture->GetFormat();
|
||||
const Bool isHalf = format == TextureInternalFormat::RGB16F;
|
||||
if (!isHalf && format != TextureInternalFormat::RGB32F) return false;
|
||||
|
||||
auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(texture.get());
|
||||
if (mipmapTexture == nullptr) return false;
|
||||
// P4a (D-M). The three-channel float mipmap fallback filters the CHAIN ON THE CPU out
|
||||
// of the frontend's level shadows and uploads the result. Monolith keeps doing exactly
|
||||
// that; a split server has no shadow to filter, and P8 is what retires it. The texels
|
||||
// themselves are the other half of ARCHITECTURE.md's generate_mipmap item, which is
|
||||
// P8's too.
|
||||
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-cpu-fallback");
|
||||
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
|
||||
constexpr Int kChannels = 3;
|
||||
|
||||
for (const auto uploadTarget : texture->GetUploadTargets()) {
|
||||
for (Uint level = 1; level < levelCount; ++level) {
|
||||
const IntVec3 srcSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, level - 1);
|
||||
const IntVec3 dstSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, level);
|
||||
if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return false;
|
||||
auto* src = static_cast<Uint8*>(mipmapTexture->MapMipmapData(uploadTarget, level - 1));
|
||||
auto* dst = static_cast<Uint8*>(mipmapTexture->MapMipmapData(uploadTarget, level));
|
||||
if (src == nullptr || dst == nullptr) return false;
|
||||
|
||||
const SizeT componentBytes = isHalf ? sizeof(Uint16) : sizeof(Float);
|
||||
const SizeT texelBytes = componentBytes * kChannels;
|
||||
const auto load = [&](const Uint8* base, Int x, Int y, Int channel) {
|
||||
const Uint8* texel = base + (static_cast<SizeT>(y) * srcSize.x() + x) * texelBytes +
|
||||
channel * componentBytes;
|
||||
if (isHalf) {
|
||||
Uint16 bits = 0;
|
||||
Memcpy(&bits, texel, sizeof(bits));
|
||||
return MG_Util::DecodeHalfBitsToFloat(bits);
|
||||
}
|
||||
Float value = 0.0f;
|
||||
Memcpy(&value, texel, sizeof(value));
|
||||
return value;
|
||||
};
|
||||
|
||||
// Box filter over the 2x2 source footprint, clamped where a dimension is
|
||||
// already 1 (GL 4.6 core 8.14.4 leaves the exact filter to the implementation
|
||||
// and this is the one it describes for power-of-two levels).
|
||||
for (Int y = 0; y < dstSize.y(); ++y) {
|
||||
for (Int x = 0; x < dstSize.x(); ++x) {
|
||||
const Int x0 = std::min(x * 2, srcSize.x() - 1);
|
||||
const Int x1 = std::min(x * 2 + 1, srcSize.x() - 1);
|
||||
const Int y0 = std::min(y * 2, srcSize.y() - 1);
|
||||
const Int y1 = std::min(y * 2 + 1, srcSize.y() - 1);
|
||||
for (Int channel = 0; channel < kChannels; ++channel) {
|
||||
const Float average = 0.25f * (load(src, x0, y0, channel) + load(src, x1, y0, channel) +
|
||||
load(src, x0, y1, channel) + load(src, x1, y1, channel));
|
||||
Uint8* texel = dst + (static_cast<SizeT>(y) * dstSize.x() + x) * texelBytes +
|
||||
channel * componentBytes;
|
||||
if (isHalf) {
|
||||
const Uint16 bits = MG_Util::EncodeFloatToHalfBits(average);
|
||||
Memcpy(texel, &bits, sizeof(bits));
|
||||
} else {
|
||||
Memcpy(texel, &average, sizeof(average));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mipmapTexture->MarkStorageDirty(uploadTarget, level, true);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// P8-B2: RGB16F / RGB32F MIP GENERATION ON THE SERVER, FROM THE SERVER'S OWN LEVEL.
|
||||
//
|
||||
// ES generates a chain only for a colour-renderable, filterable format, and whether the
|
||||
@@ -12233,30 +11905,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
};
|
||||
|
||||
// The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
|
||||
// find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject),
|
||||
// reachable from a path that has a renderbuffer but no framebuffer. The MONOLITH arm's only:
|
||||
// under a transport MakeGLESCopyImageEndpoint resolves a renderbuffer endpoint by handle and
|
||||
// never calls this. (P8-E removed the `CopyImageSubData+RENDERBUFFER` latch that stood here -
|
||||
// its transport condition could not hold at the only call site - and
|
||||
// CopyImageStoreReadbackScenario's renderbuffer case runs the handle arm on every transport.)
|
||||
static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
|
||||
if (!renderbufferObject) return nullptr;
|
||||
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
|
||||
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
|
||||
backendRenderbufferObject = *slot;
|
||||
} else {
|
||||
auto& newSlot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject);
|
||||
if (!newSlot) {
|
||||
newSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
|
||||
}
|
||||
backendRenderbufferObject = newSlot;
|
||||
}
|
||||
backendRenderbufferObject->SyncToBackend(renderbufferObject);
|
||||
return backendRenderbufferObject;
|
||||
}
|
||||
|
||||
static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y,
|
||||
GLint z, GLESCopyImageEndpoint& out) {
|
||||
if (endpoint.IsRenderbuffer()) {
|
||||
|
||||
@@ -10860,21 +10860,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The colour attachment glReadPixels/glGetTexImage would read from, or nullptr when the
|
||||
// read buffer names no colour attachment at all.
|
||||
static const MG_State::GLState::FramebufferAttachmentObject* GetReadColorAttachment() {
|
||||
const auto& readFBO =
|
||||
MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
if (!readFBO) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto readBuffer = readFBO->GetReadBuffer();
|
||||
if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) {
|
||||
return nullptr;
|
||||
}
|
||||
return &readFBO->GetAttachment(readBuffer);
|
||||
}
|
||||
|
||||
Bool IsAlphaWidenedColorAttachment(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
|
||||
if (attachmentObject.IsTexture()) {
|
||||
@@ -15798,14 +15783,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// A package REPLACES the body in place and deletes the matching comment; it does not add a
|
||||
// second definition elsewhere, or the linker's answer depends on link order.
|
||||
namespace {
|
||||
[[noreturn]] void MGPipeP5eSeamNotLanded(const char* name, const char* owner,
|
||||
MG_Pipe::MGPipeHandle handle) {
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"} - the P5e by-handle seam is declared by "
|
||||
"package c0e and bodied by package %s; it was called for handle {%u, %u} "
|
||||
"before that package landed",
|
||||
name, owner, handle.Slot, handle.Gen);
|
||||
std::abort();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace TextureImpl {
|
||||
|
||||
@@ -32,11 +32,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return MG_Config::Features.MagmaR11G11B10FFallback;
|
||||
}
|
||||
|
||||
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
(void)dpy;
|
||||
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
|
||||
}
|
||||
|
||||
Bool IsFormatIndexValid(TextureInternalFormat format) {
|
||||
return format != TextureInternalFormat::Unknown && static_cast<Int>(format) >= 0 &&
|
||||
static_cast<SizeT>(format) < kFormatCapabilityFormatCount;
|
||||
|
||||
@@ -38,10 +38,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
RendererSlot pVulkanRenderer;
|
||||
|
||||
namespace {
|
||||
void RejectWireLegacyBuffer() {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)");
|
||||
std::abort();
|
||||
}
|
||||
// Generation of the live VulkanRenderer instance, mirroring
|
||||
// DirectGLES's g_syncContextGeneration. BackendObject_DirectVulkan
|
||||
// bumps it (BumpRendererGeneration) wherever pVulkanRenderer is reset
|
||||
@@ -149,19 +145,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
|
||||
void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
if (!pixels || width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
|
||||
const auto inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
||||
const SizeT size = MG_Util::CalculateInputTextureImageSize(inputFormat, inputType,
|
||||
IntVec3(width, height, 1));
|
||||
if (size > 0) {
|
||||
std::memset(pixels, 0, size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The verb's handles identify server stores; never inspect a client binding.
|
||||
//
|
||||
@@ -243,41 +226,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
MG_State::GLState::ProgramObject* TryGetDirectVulkanProgram(GLuint program) {
|
||||
if (!MG_Pipe::gPipeInputs.ValidateProgramName(program)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto& programObject = MG_Pipe::gPipeInputs.GetProgramObject(program);
|
||||
return programObject.get();
|
||||
}
|
||||
|
||||
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
||||
auto drawBuffer = MG_Pipe::gPipeInputs.GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
drawBuffer->SyncPersistentMappedRange();
|
||||
// A command block a compute shader wrote is the case this whole shape exists
|
||||
// for, and every caller here reads the words on the CPU - the per-command
|
||||
// expansion, the baseInstance the shader emulation is fed, and the fetch
|
||||
// range a client-memory vertex array's upload is bounded by. Reconciling the
|
||||
// shadow first is what keeps those readings the real ones (a no-op unless a
|
||||
// GPU write is pending).
|
||||
drawBuffer->SyncGpuWrites();
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||
return nullptr;
|
||||
}
|
||||
return drawBuffer->MappedData() + commandOffset;
|
||||
}
|
||||
|
||||
if (!indirect) {
|
||||
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return reinterpret_cast<const Uint8*>(indirect);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): THE MONOLITH CONSUMER READS THE ARCHIVE
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
|
||||
// {slot, gen} mint the re-keyed sites are written against.
|
||||
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14). (The {slot, gen}
|
||||
// mint that used to live here keyed the frontend arm's VAO memos and went with that arm in P13
|
||||
// W6; the record arm keys every object on the applier's own handles.)
|
||||
//
|
||||
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
|
||||
//
|
||||
@@ -102,492 +103,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP2));
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H
|
||||
// site asks, so that they cannot disagree with each other or with the startup gate.
|
||||
inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) {
|
||||
// No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration
|
||||
// has already made a clear bit a startup Fatal, so the handle arm is the only arm a
|
||||
// running process can be on.
|
||||
(void)trackHBit;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// "Is the object-identity half of every vertex-input memo key deliberately defeated in
|
||||
// this run?" - the ONE question the control's sites ask, for the same reason
|
||||
// MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree,
|
||||
// and a control that defeats two of three guards proves nothing.
|
||||
//
|
||||
// WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT"
|
||||
// RATHER THAN "USE THE HEAP ADDRESS".
|
||||
//
|
||||
// D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip
|
||||
// the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at
|
||||
// the freed heap block and so reproduces the key. Measured, it does not: in
|
||||
// HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight
|
||||
// out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for
|
||||
// glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next
|
||||
// allocation the replacement path makes. Four create/delete cycles in one run produced
|
||||
// four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for
|
||||
// "hash the address" to collide with: the replacement hashes differently, indexes a
|
||||
// different memo slot, and inherits nothing - so the arm asserted stale pixels and saw
|
||||
// fresh ones, which is a FAILING negative control that had stopped controlling anything.
|
||||
//
|
||||
// So the control no longer asks the allocator for the collision; it manufactures it. On
|
||||
// both arms the object identity is replaced by a constant, which is the strongest form of
|
||||
// "the allocator handed the block back" and is deterministic. That covers strictly more
|
||||
// than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm
|
||||
// the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm
|
||||
// ships - so the replacement VAO is handed the dead one's memo entry and its content hash.
|
||||
// Defeating only the retired lifetime-id/address guards would leave that key untested,
|
||||
// which is exactly the vacuity this control exists to catch.
|
||||
//
|
||||
// WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1,
|
||||
// MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}:
|
||||
//
|
||||
// * THIS MINT still has no death notification, and that is now a narrower statement than
|
||||
// it was. P7 wave 2 package C gives DirectVulkan a StateObjectDeathOps table
|
||||
// (DirectVulkan.cpp's g_magmaStateObjectDeathOps, CONTRACT-P7 §5.5), so
|
||||
// NotifyStateObjectDestroyed DOES have a consumer on this backend - but that consumer's
|
||||
// one job is to emit the `object_death` RECORD so the server drops its twin, and it
|
||||
// never touches these per-renderer identity tables: they are the SERVER-side renderer's
|
||||
// own, the notice is raised on the CLIENT thread, and an Acquire/retire from the wrong
|
||||
// side is precisely the cross-role read CONTRACT-P5C §3.1 forbids. So a slot here still
|
||||
// returns to the free list only through OnFrameBoundary's age sweep (kSweepInterval
|
||||
// 256, kRetireAgeBoundaries 1024, below), and everything the two bullets below derive
|
||||
// from that is unchanged;
|
||||
// * HandleRecycleScenario issues five frame boundaries, so the free list is empty when
|
||||
// the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured:
|
||||
// redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
|
||||
// decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green;
|
||||
// * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last
|
||||
// draw, which necessarily puts the two draws in different frames - and the only memo
|
||||
// that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines
|
||||
// across frames by design. The two requirements are mutually exclusive, so the
|
||||
// generation is out of reach of any same-frame pixel reproducer for this memo.
|
||||
//
|
||||
// The generation is covered where it IS expressible, over this mint and the claim rule
|
||||
// MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real
|
||||
// retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at
|
||||
// {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that
|
||||
// suite; it is the only place in the tree where that deletion is caught.
|
||||
//
|
||||
// Everything the control does NOT defeat is as load-bearing as what it does. It never
|
||||
// touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame
|
||||
// serial, its slice-epoch compares and its host-map check all stay in force, so a green
|
||||
// AbaControl arm still means "a replacement object was handed its dead predecessor's
|
||||
// resolved vertex bindings because the identity halves of the keys were defeated", not
|
||||
// "every safety net was switched off until something broke".
|
||||
//
|
||||
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
|
||||
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
|
||||
// P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them".
|
||||
//
|
||||
// P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso,
|
||||
// SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half
|
||||
// of P4a's memo keys as well", because a control that only defeats the guards a phase
|
||||
// RETIRED says nothing about the key that phase SHIPS.
|
||||
//
|
||||
// On Magma there is no such key to defeat, and that is a fact about the roadmap rather than
|
||||
// an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds,
|
||||
// VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program
|
||||
// are all still reached from their frontend objects on this backend, and moving them onto
|
||||
// handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan
|
||||
// untouched apart from this file). So the honest statement is per KIND, and it is spelled as
|
||||
// code rather than as a comment so that a caller cannot read the blanket answer above and
|
||||
// conclude the knob covers its kind:
|
||||
//
|
||||
// * for the two kinds this backend really keys on {slot, gen}, the knob defeats the
|
||||
// identity exactly as it always has (MagmaPipeClaimSlotMemos);
|
||||
// * for P4a's six there is nothing here to defeat, so the answer is FALSE - and
|
||||
// MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and
|
||||
// makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is
|
||||
// not controlling anything for that kind. It does not assert a corruption that no code
|
||||
// on this tree can produce, which would be a permanently red always-on lane.
|
||||
//
|
||||
// WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it:
|
||||
// when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot
|
||||
// tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already
|
||||
// has for vertex input - this function's per-kind answer becomes that consumer's, the
|
||||
// integration probe finds the consumer, and the six cases flip to expecting the corruption.
|
||||
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
|
||||
return MG_Config::Features.PipeHandleAbaControl;
|
||||
}
|
||||
|
||||
// WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob
|
||||
// above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind.
|
||||
//
|
||||
// EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason:
|
||||
// a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this
|
||||
// file rather than a row that silently inherits somebody else's answer. Being wrong in the
|
||||
// "covered" direction is the expensive one - a control asserting a corruption nobody can
|
||||
// produce is a permanently red always-on lane - so an undecided kind must never read true,
|
||||
// and with no `default:` there is no arm for it to read true from.
|
||||
//
|
||||
// constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a
|
||||
// predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the
|
||||
// asserts say so in both directions, and the file no longer compiles if the tables and this
|
||||
// statement of them ever part company. (Review F-m5: the earlier form had no caller at all
|
||||
// and could not make anything red or green.)
|
||||
inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) {
|
||||
switch (kind) {
|
||||
// The two MagmaPipeIdentityTables really mints.
|
||||
case MG_Pipe::MGPipeKind::VertexElementsCso:
|
||||
case MG_Pipe::MGPipeKind::Buffer:
|
||||
return true;
|
||||
// P4a's six object classes: still reached from their frontend objects on this
|
||||
// backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for
|
||||
// the knob to defeat.
|
||||
case MG_Pipe::MGPipeKind::Texture:
|
||||
case MG_Pipe::MGPipeKind::Renderbuffer:
|
||||
case MG_Pipe::MGPipeKind::Framebuffer:
|
||||
case MG_Pipe::MGPipeKind::SamplerCso:
|
||||
case MG_Pipe::MGPipeKind::SamplerViewCso:
|
||||
case MG_Pipe::MGPipeKind::ShaderCso:
|
||||
// ...and everything else this backend does not mint a handle for.
|
||||
case MG_Pipe::MGPipeKind::None:
|
||||
case MG_Pipe::MGPipeKind::Xfb:
|
||||
case MG_Pipe::MGPipeKind::RenderStateCso:
|
||||
case MG_Pipe::MGPipeKind::Fence:
|
||||
case MG_Pipe::MGPipeKind::Query:
|
||||
case MG_Pipe::MGPipeKind::Context:
|
||||
case MG_Pipe::MGPipeKind::KindCount:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso),
|
||||
"MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to "
|
||||
"defeat for it");
|
||||
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer),
|
||||
"MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it");
|
||||
static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso),
|
||||
"P4a's six object classes are not keyed on {slot, gen} on this backend, so "
|
||||
"HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. "
|
||||
"Wiring one of them is what flips this assert, this predicate and that arm - and "
|
||||
"MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what "
|
||||
"carries the answer into the lane");
|
||||
|
||||
// THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier
|
||||
// round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two
|
||||
// statements above - and it had no caller anywhere in the tree: the knob's only two
|
||||
// consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline
|
||||
// key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a
|
||||
// variable at either site. A conjunction no build ever evaluates cannot be pinned the way
|
||||
// the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features,
|
||||
// so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about,
|
||||
// one level up: an `&&` whose operands could be inverted or dropped with nothing to say so.
|
||||
//
|
||||
// The two pieces stand alone instead, and each is pinned by something that runs:
|
||||
// MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the
|
||||
// three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats
|
||||
// Identity is the knob, and its two consumers are what make it true or false. A call site
|
||||
// that ever does hold a variable kind writes the `&&` there, where a build will run it.
|
||||
|
||||
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
|
||||
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
|
||||
// handle onto it), so nothing about the tables changes shape for the control's sake.
|
||||
inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The {slot, gen} mint
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
|
||||
// properties, and the third is the one review v2 got wrong:
|
||||
//
|
||||
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
|
||||
// never match a live object even if the allocator hands back the same heap address
|
||||
// (the ABA HandleRecycleScenario reproduces);
|
||||
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
|
||||
// no probe and no mix;
|
||||
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
|
||||
// drawn, whatever the working set size.
|
||||
//
|
||||
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
|
||||
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
|
||||
// and every consumer memo keyed on the handle died with it: measured on a verbatim
|
||||
// transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and
|
||||
// 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys
|
||||
// VAOs, which is the Minecraft chunk shape this exists for). Two of the three memos it
|
||||
// fed - the content-hash memo and the resolved-state memo - had NO capacity before this
|
||||
// package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction
|
||||
// there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the
|
||||
// buffer table thrashes too, makes the vertex-input content hash a per-draw value that
|
||||
// inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every
|
||||
// draw. That is a worse leak than the one it was introduced to avoid.
|
||||
//
|
||||
// So: grow on demand, and reclaim by AGE instead of by capacity.
|
||||
//
|
||||
// * Acquire hits an UnorderedMap<lifetimeId, slotIndex>, in front of which sits a
|
||||
// one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo
|
||||
// turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64
|
||||
// compares - less than the address multiply plus two-way probe the pre-handle arm ran.
|
||||
// * OnFrameBoundary retires slots whose object has not been drawn for
|
||||
// kRetireAgeBoundaries boundaries and returns them to a free list, so the table's
|
||||
// footprint tracks the LIVE DRAWN working set, not objects ever created. That is the
|
||||
// property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its
|
||||
// Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed
|
||||
// a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no
|
||||
// death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's
|
||||
// six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState
|
||||
// plus one map node per object EVER created, for the life of the process, on a
|
||||
// platform with an LMK. Age-based reclamation is the stand-in for the death
|
||||
// notification, and it is exactly as ABA-proof, because reuse bumps Gen.
|
||||
// * A retire costs at most one memo recompute if the object is drawn again - the same
|
||||
// price a cache miss costs - and it is charged only to objects that went idle for
|
||||
// ~1024 frames, never to a hot one.
|
||||
//
|
||||
// Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against
|
||||
// the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is
|
||||
// no capacity to size off a device measurement because there is no capacity; what the
|
||||
// device run in D.4.2 can still want is the number itself, so the high-water mark is
|
||||
// logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a
|
||||
// draw - ROADMAP.md:7).
|
||||
//
|
||||
// Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see
|
||||
// MagmaPipeIdentityTables): a process-global would share one table, and one reclamation
|
||||
// clock, across two live contexts.
|
||||
class MagmaPipeIdentityTable {
|
||||
public:
|
||||
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
|
||||
|
||||
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
|
||||
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
|
||||
Uint32 Count() const { return static_cast<Uint32>(m_entries.size()); }
|
||||
// Objects currently holding a slot - the live working set this table tracks.
|
||||
Uint32 LiveCount() const { return static_cast<Uint32>(m_index.size()); }
|
||||
|
||||
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
|
||||
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
|
||||
// zero-initialised memo slot cannot carry a live object's id. Guarded anyway so
|
||||
// that a zero can never be minted into a slot and then indexed with.
|
||||
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
|
||||
// The one-entry front memo. Cleared by any retire, so it can never serve a slot
|
||||
// that has been handed back to the free list.
|
||||
if (lifetimeId == m_lastLifetimeId) {
|
||||
m_entries[m_lastIndex].LastUse = m_boundary;
|
||||
return m_lastHandle;
|
||||
}
|
||||
Uint32 index = 0;
|
||||
const auto it = m_index.find(lifetimeId);
|
||||
if (it != m_index.end()) {
|
||||
index = it->second;
|
||||
} else {
|
||||
index = ClaimSlot();
|
||||
m_entries[index].LifetimeId = lifetimeId;
|
||||
m_index.emplace(lifetimeId, index);
|
||||
}
|
||||
Entry& entry = m_entries[index];
|
||||
entry.LastUse = m_boundary;
|
||||
m_lastLifetimeId = lifetimeId;
|
||||
m_lastIndex = index;
|
||||
m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
|
||||
entry.Gen};
|
||||
return m_lastHandle;
|
||||
}
|
||||
|
||||
// Ages the table and returns idle slots to the free list. Same shape and the same
|
||||
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
|
||||
// slots' consumers use.
|
||||
void OnFrameBoundary() {
|
||||
++m_boundary;
|
||||
if ((m_boundary % kSweepInterval) != 0) return;
|
||||
SizeT retired = 0;
|
||||
for (auto it = m_index.begin(); it != m_index.end();) {
|
||||
Entry& entry = m_entries[it->second];
|
||||
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
|
||||
entry.LifetimeId = 0;
|
||||
m_freeSlots.push_back(it->second);
|
||||
it = m_index.erase(it);
|
||||
++retired;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (retired != 0) {
|
||||
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
|
||||
// so a front memo pointing at one would still hand out a handle the consumer
|
||||
// tables would accept. Drop it.
|
||||
m_lastLifetimeId = 0;
|
||||
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
|
||||
m_kindName, retired, LiveCount(), Count());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Sweep cadence and retirement age, deliberately the same numbers
|
||||
// VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot
|
||||
// retired earlier than its cache entry would mint a new handle for an object whose
|
||||
// entry is still live and still correct, which is a pure waste.
|
||||
static constexpr Uint64 kSweepInterval = 256;
|
||||
static constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
|
||||
struct Entry {
|
||||
Uint64 LifetimeId = 0;
|
||||
Uint64 LastUse = 0;
|
||||
// Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps
|
||||
// its generation, which is what makes a memo survive a reconfiguration.
|
||||
Uint32 Gen = 0;
|
||||
};
|
||||
|
||||
Uint32 ClaimSlot() {
|
||||
while (!m_freeSlots.empty()) {
|
||||
const Uint32 index = m_freeSlots.back();
|
||||
m_freeSlots.pop_back();
|
||||
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
|
||||
// MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are
|
||||
// live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the
|
||||
// RELEASE path instead of asserted: a slot that has been reused 2^32 times is
|
||||
// permanently retired rather than wrapped, because a wrapped Gen would let a
|
||||
// stale handle match a live object. It costs one slot.
|
||||
if (m_entries[index].Gen == ~Uint32{0}) {
|
||||
MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is "
|
||||
"retired for good; {slot, gen} stays unique",
|
||||
m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot);
|
||||
continue;
|
||||
}
|
||||
++m_entries[index].Gen;
|
||||
return index;
|
||||
}
|
||||
const Uint32 index = static_cast<Uint32>(m_entries.size());
|
||||
m_entries.push_back(Entry{});
|
||||
m_entries[index].Gen = 1;
|
||||
// The high-water mark, at powers of two from 1024 up: at most a handful of lines
|
||||
// for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per
|
||||
// object this backend has ever seen and never on a draw (ROADMAP.md:7).
|
||||
//
|
||||
// [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This
|
||||
// one is I, not D, because D is compiled out of every build that ships and of every
|
||||
// build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for:
|
||||
// the live-object high-water mark of minecraft-1.21.4-in-world and
|
||||
// ...-sodium-in-world, which nothing on desktop reaches and no gate here can see.
|
||||
// The structure no longer has a capacity to size off it, so the number is evidence
|
||||
// rather than a tuning input - but D.4.2 should still read it out of the device log,
|
||||
// and it cannot read a line that was compiled away.
|
||||
const SizeT minted = m_entries.size();
|
||||
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
|
||||
MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
|
||||
m_kindName, minted, LiveCount());
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
const char* m_kindName = "";
|
||||
Uint64 m_boundary = 0;
|
||||
Vector<Entry> m_entries;
|
||||
Vector<Uint32> m_freeSlots;
|
||||
UnorderedMap<Uint64, Uint32> m_index;
|
||||
// One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live
|
||||
// object's lifetime id is never 0.
|
||||
Uint64 m_lastLifetimeId = 0;
|
||||
Uint32 m_lastIndex = 0;
|
||||
MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
};
|
||||
|
||||
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
|
||||
// a context recreation, which destroys and rebuilds the renderer) would otherwise share one
|
||||
// table and one reclamation clock, and both consumer tables are per-instance already.
|
||||
class MagmaPipeIdentityTables {
|
||||
public:
|
||||
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array
|
||||
// resolves to, and the only kind in MGPipeKind that names vertex-input state.
|
||||
MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
||||
return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId)
|
||||
: m_vaos.Acquire(lifetimeId);
|
||||
}
|
||||
void OnFrameBoundary() {
|
||||
m_vaos.OnFrameBoundary();
|
||||
m_buffers.OnFrameBoundary();
|
||||
}
|
||||
const MagmaPipeIdentityTable& Vaos() const { return m_vaos; }
|
||||
const MagmaPipeIdentityTable& Buffers() const { return m_buffers; }
|
||||
|
||||
private:
|
||||
MagmaPipeIdentityTable m_vaos{"VertexElementsCso"};
|
||||
MagmaPipeIdentityTable m_buffers{"Buffer"};
|
||||
};
|
||||
|
||||
// The table entry a handle names. Every per-slot table Magma keeps is indexed by this.
|
||||
//
|
||||
// A null handle has no slot, and it is unreachable here: both lifetime-id sources start at
|
||||
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
|
||||
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
|
||||
// MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null
|
||||
// handle can never match, because MGPipeHandleIsNull is also what the consumers compare.
|
||||
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"a null MGPipeHandle has no slot to index a per-slot table with");
|
||||
return MG_Pipe::MGPipeHandleIsNull(handle)
|
||||
? 0u
|
||||
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
|
||||
}
|
||||
|
||||
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
|
||||
//
|
||||
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
|
||||
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
|
||||
// references into these entries across nested calls. Chunks of kChunkEntries are appended
|
||||
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
|
||||
// reference is valid for the life of the table. That is the same guarantee the fixed table
|
||||
// it replaces gave, without the fixed capacity.
|
||||
template <typename T, Uint32 kChunkEntries = 256>
|
||||
class MagmaPipeSlotTable {
|
||||
public:
|
||||
T& operator[](Uint32 index) {
|
||||
const Uint32 chunk = index / kChunkEntries;
|
||||
while (m_chunks.size() <= chunk) {
|
||||
m_chunks.push_back(MakeUnique<Chunk>());
|
||||
}
|
||||
return m_chunks[chunk]->Entries[index % kChunkEntries];
|
||||
}
|
||||
SizeT Capacity() const { return m_chunks.size() * kChunkEntries; }
|
||||
|
||||
private:
|
||||
struct Chunk {
|
||||
T Entries[kChunkEntries] = {};
|
||||
};
|
||||
Vector<UniquePtr<Chunk>> m_chunks;
|
||||
};
|
||||
|
||||
// The claim rule every per-slot memo table uses, in one place so that the rule and the
|
||||
// negative control that defeats it cannot drift apart between consumers - and so that the
|
||||
// unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests
|
||||
// this code rather than a copy of it.
|
||||
//
|
||||
// The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is
|
||||
// this object's. A slot the mint recycled for a different object comes back with a moved
|
||||
// Gen, so the compare fails and the entry is cleared rather than inherited. That is the
|
||||
// half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity).
|
||||
//
|
||||
// With negative control C on, every object collapses onto one entry and the entry is handed
|
||||
// back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's
|
||||
// slot" and "the slot was reused and Gen did not move".
|
||||
//
|
||||
// `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that
|
||||
// means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance.
|
||||
template <typename Memos, Uint32 kChunkEntries>
|
||||
inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable<Memos, kChunkEntries>& table,
|
||||
const MG_Pipe::MGPipeHandle& handle) {
|
||||
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||
return table[kMagmaPipeAbaControlSlotIndex];
|
||||
}
|
||||
Memos& memos = table[MagmaPipeSlotIndex(handle)];
|
||||
if (!(memos.Owner == handle)) {
|
||||
memos = Memos{};
|
||||
memos.Owner = handle;
|
||||
}
|
||||
return memos;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -19,36 +19,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
class MagmaProgramSource {
|
||||
public:
|
||||
using Program = MG_State::GLState::ProgramObject;
|
||||
MagmaProgramSource(const Program& program) : m_frontend(&program) {}
|
||||
MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record)
|
||||
: m_handle(handle), m_record(&record) {
|
||||
MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive");
|
||||
}
|
||||
|
||||
Bool IsWire() const { return m_record != nullptr; }
|
||||
MG_Pipe::MGPipeHandle Handle() const { return m_handle; }
|
||||
const Program* Frontend() const { return m_frontend; }
|
||||
Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); }
|
||||
Uint GetExternalIndex() const { return m_handle.Slot; }
|
||||
Uint64 GetLifetimeId() const {
|
||||
return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId();
|
||||
return (Uint64(m_handle.Gen) << 32) | m_handle.Slot;
|
||||
}
|
||||
Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); }
|
||||
Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); }
|
||||
Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); }
|
||||
Bool GetLinkStatus() const { return m_record->Desc.LinkStatus != 0; }
|
||||
Bool GetSpirvStatus() const { return m_record->Desc.SpirvStatus != 0; }
|
||||
Bool PointSizeDemoted() const { return m_record->Desc.PointSizeDemoted != 0; }
|
||||
Bool GetSpirvValidationEnabled() const {
|
||||
return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled();
|
||||
return m_record->Desc.EnableSpirvValidation != 0;
|
||||
}
|
||||
const Vector<Vector<Uint>>& GetGeneratedSpirv() const {
|
||||
return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv();
|
||||
return m_record->Archive->Spirv.generatedSpirv;
|
||||
}
|
||||
Vector<ShaderStage> GetLinkedShaderStages() const {
|
||||
if (!IsWire()) return m_frontend->GetLinkedShaderStages();
|
||||
Vector<ShaderStage> result;
|
||||
for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast<ShaderStage>(stage));
|
||||
return result;
|
||||
}
|
||||
Bool HasLinkedShaderStage(ShaderStage stage) const {
|
||||
if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage);
|
||||
const auto& stages = m_record->Archive->LinkedStages;
|
||||
return std::find(stages.begin(), stages.end(), static_cast<Uint32>(stage)) != stages.end();
|
||||
}
|
||||
@@ -56,65 +51,58 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// A view has no lifetime in which a memo can safely persist. The server
|
||||
// factory's content cache still owns compiled programs; hash from current
|
||||
// archive + binding tails rather than borrowing the client's mutable memo.
|
||||
return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash);
|
||||
(void)flags;
|
||||
(void)hash;
|
||||
return false;
|
||||
}
|
||||
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
|
||||
if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash);
|
||||
}
|
||||
Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); }
|
||||
Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); }
|
||||
Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); }
|
||||
Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); }
|
||||
Uint64 GetBackendStateVersion() const { return m_record->Serial; }
|
||||
Uint64 GetBlockBindingVersion() const { return m_record->BindingsSerial; }
|
||||
Uint64 GetImageUnitVersion() const { return m_record->BindingsSerial; }
|
||||
Uint64 GetLinkVersion() const { return m_record->Serial; }
|
||||
|
||||
const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); }
|
||||
Uint GetUBOSize() const { return IsWire() ? static_cast<Uint>(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); }
|
||||
Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); }
|
||||
const void* GetUBOData() const { return m_record->GlobalConstants.data(); }
|
||||
Uint GetUBOSize() const { return static_cast<Uint>(m_record->GlobalConstants.size()); }
|
||||
Uint32 GetUBOContentVersion() const { return m_record->GlobalConstantsVersion; }
|
||||
Int GetActiveUniformBlocksCount() const {
|
||||
return IsWire() ? static_cast<Int>(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount();
|
||||
return static_cast<Int>(Link().glBlockIndexToTProgram.size());
|
||||
}
|
||||
Uint GetUniformBlockIndex(const char* name) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockIndex(name);
|
||||
auto found = Link().uniformBlockIndexByName.find(name);
|
||||
if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]");
|
||||
return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second;
|
||||
}
|
||||
Uint GetUniformBlockBinding(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockBinding(index);
|
||||
return index < m_record->BlockBindings.size() ? static_cast<Uint>(m_record->BlockBindings[index]) : 0;
|
||||
}
|
||||
const String& GetUniformBlockName(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockName(index);
|
||||
const auto* block = Block(index);
|
||||
static const String empty;
|
||||
return block ? block->name : empty;
|
||||
}
|
||||
Uint GetUBOSizeAt(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUBOSizeAt(index);
|
||||
const auto* block = Block(index);
|
||||
return block ? (static_cast<Uint>(block->size) + 15u) & ~15u : 0;
|
||||
}
|
||||
Bool IsValidUniformLocation(Int location) const {
|
||||
return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location);
|
||||
return Program::IsValidUniformLocation(Link(), location);
|
||||
}
|
||||
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
|
||||
if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b);
|
||||
return IsValidUniformLocation(a) && IsValidUniformLocation(b) &&
|
||||
Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b];
|
||||
}
|
||||
GLenum GetUniformType(Uint location) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformType(location);
|
||||
return IsValidUniformLocation(static_cast<Int>(location))
|
||||
? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0;
|
||||
}
|
||||
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location);
|
||||
const auto& units = m_record->SamplerUnits;
|
||||
const auto found = std::lower_bound(units.begin(), units.end(), location,
|
||||
[](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; });
|
||||
return found != units.end() && found->Location == location ? found->Unit : -1;
|
||||
}
|
||||
Int GetUniformLocation(const String& name) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformLocation(name);
|
||||
const auto& locations = Link().uniformLocations;
|
||||
auto found = locations.find(name);
|
||||
if (found != locations.end()) return static_cast<Int>(found->second);
|
||||
@@ -142,17 +130,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return UniformLocationsAliasSameUniform(base, location) ? location : -1;
|
||||
}
|
||||
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); }
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return Link().xfbVaryings.size(); }
|
||||
const Vector<MG_State::GLState::XfbVarying>& GetTransformFeedbackVaryings() const {
|
||||
return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings();
|
||||
return Link().xfbVaryings;
|
||||
}
|
||||
SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); }
|
||||
SizeT GetTransformFeedbackBufferCount() const { return Link().xfbStrides.size(); }
|
||||
Uint32 GetTransformFeedbackStride(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index);
|
||||
return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0;
|
||||
}
|
||||
GLenum GetTransformFeedbackBufferMode() const {
|
||||
return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode();
|
||||
return Link().xfbBufferMode;
|
||||
}
|
||||
// P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): READ OUT OF THE ARCHIVE.
|
||||
//
|
||||
@@ -164,7 +151,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the archive (LinkArtifacts::storageBlocks), which takes the SPIR-V reflector off the
|
||||
// wire draw path entirely - this header no longer includes spirv_reflect.h at all.
|
||||
Uint GetShaderStorageBlockIndex(const String& name) const {
|
||||
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name);
|
||||
const auto& blocks = Link().storageBlocks;
|
||||
// The archive carries NORMALISED names (the array subscript stripped), because
|
||||
// that is the spelling SPIRV-Reflect's type_name gives and the spelling
|
||||
@@ -185,7 +171,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
: static_cast<Uint>(found - blocks.begin());
|
||||
}
|
||||
Uint GetShaderStorageBlockBinding(Uint index) const {
|
||||
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index);
|
||||
const auto& blocks = Link().storageBlocks;
|
||||
if (index >= blocks.size()) return 0;
|
||||
const auto& block = blocks[index];
|
||||
@@ -207,7 +192,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return tIndex >= 0 && static_cast<SizeT>(tIndex) < Link().blockReflection.size()
|
||||
? &Link().blockReflection[tIndex] : nullptr;
|
||||
}
|
||||
const Program* m_frontend = nullptr;
|
||||
MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle;
|
||||
const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr;
|
||||
// P7 OQ-8: the two mutables that memoised the per-draw reflect are gone with it. They
|
||||
|
||||
@@ -2235,28 +2235,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
TextureTarget ReflectImageTraitsToTextureTarget(const SpvReflectImageTraits& imageTraits) {
|
||||
switch (imageTraits.dim) {
|
||||
case SpvDim1D:
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::Texture1DArray : TextureTarget::Texture1D;
|
||||
case SpvDim2D:
|
||||
if (imageTraits.ms != 0) {
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::Texture2DMultisampleArray
|
||||
: TextureTarget::Texture2DMultisample;
|
||||
}
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::Texture2DArray : TextureTarget::Texture2D;
|
||||
case SpvDim3D:
|
||||
return TextureTarget::Texture3D;
|
||||
case SpvDimCube:
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::TextureCubeMapArray : TextureTarget::TextureCubeMap;
|
||||
case SpvDimBuffer:
|
||||
return TextureTarget::TextureBuffer;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "ProgramFactory: unsupported sampler image dim %d", imageTraits.dim);
|
||||
return TextureTarget::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsFloatStorageImageUniformType(GLenum uniformType) {
|
||||
switch (uniformType) {
|
||||
case GL_IMAGE_1D:
|
||||
@@ -3495,7 +3473,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// contains a dynamic buffer descriptor (VUID 03001/03011). Wire draws
|
||||
// retain UniformManager's per-frame, content-versioned descriptor sets;
|
||||
// this selects the legal ordinary pool, not in-place mutation of a live set.
|
||||
(!program.IsWire() || entry.dynamicBindings.empty()) &&
|
||||
entry.dynamicBindings.empty() &&
|
||||
uab.enabled && updateAfterBindSamplers <= uab.maxPerStageSamplers &&
|
||||
updateAfterBindUniformBuffers <= uab.maxPerStageUniformBuffers &&
|
||||
updateAfterBindStorageBuffers <= uab.maxPerStageStorageBuffers &&
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,25 +100,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 textureLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
};
|
||||
Bool CollectSampledTextures(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
||||
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
|
||||
// Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures
|
||||
// walk and reports whether every visited binding still resolves to the recorded
|
||||
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
|
||||
// redundant glBindSampler, which always bumps it) does not prove the sampled set
|
||||
// moved; this walk does, without rebuilding the set or falling off the fast path.
|
||||
Bool SampledBindingsUnchanged(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
const Vector<SampledBindingRecord>& previousRecords) const;
|
||||
Bool CollectStorageImageTextures(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||
Bool CollectSamplerImageFeedback(
|
||||
const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<SamplerImageFeedbackBinding>& outBindings) const;
|
||||
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
||||
GLint imageLevel, GLenum imageAccess);
|
||||
// Resolve lazy texture uploads/promotions before the caller captures the
|
||||
@@ -147,16 +128,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
|
||||
VkFormat resourceFormat, Bool useBindingFormat);
|
||||
|
||||
// True when the program reads at least one sampler and every one of them is bound to a
|
||||
// texture whose GL level range is a single level. Such a sampler resolves to
|
||||
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
|
||||
// and an explicit LOD 0 sample must read the same texel - which is what makes the
|
||||
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
|
||||
// only GL state, so a texture that ends up single-level for another reason (one uploaded
|
||||
// level under a wide level range) merely misses the rewrite.
|
||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
// ---- per-frame descriptor pools ----------------------------------------------------
|
||||
// Each frame slot owns a list of pools. The first is the base pool (non-update-after-bind,
|
||||
// setsPerFrame sets) and is never destroyed while the manager lives; every later one was
|
||||
@@ -290,36 +261,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 quietEpochs = 0;
|
||||
};
|
||||
|
||||
static Bool ResolveSamplerTexture(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
// Shared per-binding resolution for CollectSampledTextures and
|
||||
// SampledBindingsUnchanged, so membership and comparison can never diverge:
|
||||
// texture after the fallback substitution (may still be null when no fallback
|
||||
// exists), effective sampler = unit override else the texture's own sampler.
|
||||
// False = the binding is skipped (unbound with a non-2D fallback target).
|
||||
// `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor.
|
||||
Bool ResolveSampledBinding(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
|
||||
MG_State::GLState::ITextureObject*& outTexture,
|
||||
const MG_State::GLState::SamplerObject*& outSampler) const;
|
||||
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
|
||||
// the bound texture stays alive through the draw via GL binding state, so callers that
|
||||
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||
const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
|
||||
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
|
||||
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
|
||||
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
|
||||
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
|
||||
// class outright rather than being reinterpreted at view time.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
|
||||
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
|
||||
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
|
||||
@@ -336,16 +277,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// class when the device cannot use the declared one as a texel buffer.
|
||||
VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain,
|
||||
Bool storage);
|
||||
// A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the
|
||||
// view the descriptor gets has the view type the shader's image declaration demands.
|
||||
// Null for a target with no single-sampled placeholder shape - multisample images, whose
|
||||
// descriptor needs a multisample view that this cannot stand in for.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,
|
||||
VkFormat format) const;
|
||||
// The (target, format) pair a storage-image binding's placeholder is keyed by, resolved
|
||||
// from reflection alone. False when the binding has no placeholder shape.
|
||||
Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
TextureTarget& outTarget, VkFormat& outFormat) const;
|
||||
// `element` indexes a sampler ARRAY inside one binding; each element carries its own
|
||||
// independently assigned GL texture unit, so it selects the texture, the sampler
|
||||
// override and the fallback separately from its neighbours.
|
||||
@@ -475,19 +406,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 m_descriptorSlotTrims = 0;
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
|
||||
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
|
||||
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
|
||||
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
|
||||
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
|
||||
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
|
||||
// See AcquireUnboundTexelBufferView. Lazily populated, never evicted (a program's declared
|
||||
// formats are a fixed, tiny set) and torn down with the manager. The texel views are keyed
|
||||
// by format AND by storage-vs-sampled
|
||||
// because the two descriptor kinds demand different format FEATURES of the device, so one
|
||||
// format can be usable for one and not the other. Deliberately NOT the per-frame
|
||||
// texelBufferViews list: those are destroyed at every frame boundary, and these must
|
||||
// outlive it or the placeholder would be rebuilt for every unbound binding every frame.
|
||||
UnorderedMap<Uint64, VkBufferView> m_unboundTexelBufferViews;
|
||||
mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;
|
||||
|
||||
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
|
||||
// capacity) so the descriptor-write path stops allocating on every draw.
|
||||
|
||||
@@ -18,382 +18,6 @@
|
||||
#include <utility>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::ComputeHash(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
|
||||
for (Int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) {
|
||||
const auto& attr = vao.GetAttribute(i);
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Enabled, sizeof(attr.Enabled)));
|
||||
if (!attr.Enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Size, sizeof(attr.Size)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Type, sizeof(attr.Type)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Normalized, sizeof(attr.Normalized)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
||||
|
||||
// The bound buffer's IDENTITY is a component of the key, and it has to be the
|
||||
// buffer's never-reused lifetime id - NOT its heap address, which this used to
|
||||
// hash. An address is recycled by the allocator, so a deleted-and-recreated
|
||||
// buffer reproduces it; combined with a byte-identical attribute layout that
|
||||
// reproduces the WHOLE content hash, and the hash is what
|
||||
// TryBindResolvedVertexBindings accepts as proof that a memoised binding still
|
||||
// reads the buffer it was resolved from. It did not: a destroyed buffer's GPU
|
||||
// slice was bound for its successor's draw, which is how a transform-feedback
|
||||
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
|
||||
// test's positions) instead of its own.
|
||||
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
||||
//
|
||||
// P2 D12.4 / ARCHITECTURE.md 9.5: under the handle arm the identity is the
|
||||
// buffer's {slot, gen} rather than its lifetime id - "lifetimeId -> gen mixed
|
||||
// into every server-side content hash". The two are equally ABA-proof (the
|
||||
// allocator maps one onto the other and bumps Gen only on slot REUSE); what
|
||||
// changes is that the key is now the identity the SERVER will be handed once
|
||||
// buffers travel as handles, instead of a number only the client can mint.
|
||||
Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||
if (attr.Buffer) {
|
||||
// The SAME arm question the other four re-keyed sites ask, through the same
|
||||
// helper: a site that decided for itself could silently key on the pre-handle
|
||||
// identity while its neighbours keyed on the handle.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
m_identity->HandleOf(MG_Pipe::MGPipeKind::Buffer, attr.Buffer->GetLifetimeId());
|
||||
bufferKey = static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
|
||||
}
|
||||
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||
// Negative control C (P2 brief D18), on WHICHEVER arm this run is on - the
|
||||
// pre-handle lifetime id and the handle's {slot, gen} are the same guard
|
||||
// wearing two hats, and a control that defeated only the retired one would
|
||||
// say nothing about the key P2 ships.
|
||||
//
|
||||
// The identity is replaced by a constant rather than by the raw
|
||||
// BufferObject*, because the address is not recycled in practice and so
|
||||
// never collides (see MagmaPipeAbaControlDefeatsIdentity). Zero is what a
|
||||
// key with NO buffer identity in it looks like - the exact defect this
|
||||
// hash was fixed for: "the hash is what TryBindResolvedVertexBindings
|
||||
// accepts as proof that a memoised binding still reads the buffer it was
|
||||
// resolved from", and with the identity gone it accepts a binding resolved
|
||||
// from a different buffer. HandleRecycleScenario.AbaControl then draws a
|
||||
// replacement VAO and gets its dead predecessor's vertex data.
|
||||
bufferKey = 0;
|
||||
}
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
m_identity->HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
|
||||
// One entry per mintable slot, grown on demand: the mint has no capacity, so neither
|
||||
// does this, and no two live VAOs can share an entry however large the working set is.
|
||||
// There is no probe in front of it because the mint itself is one - a one-entry memo
|
||||
// hit for every acquisition after this draw's first, and a hash probe otherwise.
|
||||
//
|
||||
// The claim rule - the slot picks the entry, the whole handle (Gen included) decides
|
||||
// whose it is - and negative control C's defeat of it are MagmaPipeArms.h's
|
||||
// MagmaPipeClaimSlotMemos, so that the unit suite which drives a REAL slot reuse
|
||||
// (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) exercises this code and not a copy of it.
|
||||
// What the control defeats HERE is the identity that SELECTS the entry: every VAO
|
||||
// collapses onto one, handed back uncleared, so the replacement inherits the dead
|
||||
// VAO's content hash and its resolved-entry pointer. The GENERATION half is the unit
|
||||
// suite's business, for the reason MagmaPipeAbaControlDefeatsIdentity spells out.
|
||||
return MagmaPipeClaimSlotMemos(m_vaoMemos, handle);
|
||||
}
|
||||
|
||||
Bool VertexInputStateFactory::TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao,
|
||||
Uint64& outHash) const {
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
const VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.HashConfigVersion != vao.GetConfigVersion()) return false;
|
||||
outHash = memos.Hash;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
HashType hash = 0;
|
||||
// P2 D12.5: the same memo, on the backend's side of the boundary.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.HashConfigVersion == vao.GetConfigVersion()) {
|
||||
return memos.Hash;
|
||||
}
|
||||
hash = ComputeHash(vao);
|
||||
memos.Hash = hash;
|
||||
memos.HashConfigVersion = vao.GetConfigVersion();
|
||||
return hash;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao) {
|
||||
// P2 D12.5: the same per-draw fast path, but the resolved-entry pointer lives in this
|
||||
// factory's slot-indexed table instead of on the frontend VAO. The eviction epoch
|
||||
// survives the move and is still what stops a stale pointer being dereferenced: the
|
||||
// POINTEE is a cache entry this factory can erase at a frame boundary, and moving the
|
||||
// memo does not change that.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.StateConfigVersion == vao.GetConfigVersion() && memos.State != nullptr &&
|
||||
memos.StateEpoch == m_evictionEpoch) {
|
||||
const auto* memoEntry = static_cast<const BackendVertexInputState*>(memos.State);
|
||||
memoEntry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *memoEntry;
|
||||
}
|
||||
const BackendVertexInputState& resolved =
|
||||
GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
// MemosFor is re-taken rather than kept live across GetOrCreateVertexInputState:
|
||||
// the reference is not worth holding across a call that can resize the table.
|
||||
VaoBackendMemos& stamp = MemosFor(vao);
|
||||
stamp.State = &resolved;
|
||||
stamp.StateEpoch = m_evictionEpoch;
|
||||
stamp.StateConfigVersion = vao.GetConfigVersion();
|
||||
// The AUX memo is deliberately NOT stamped here: its two words already live in
|
||||
// VulkanRenderer::VaoDrawMemo (layoutHash / layoutAuxMasks) and its getter has no
|
||||
// live reader anywhere, so the handle arm retires it rather than moving it.
|
||||
return resolved;
|
||||
}
|
||||
// Unreachable: with no legacy arm compiled MagmaPipeTrackHArmIsHandles is a compile-
|
||||
// time true, so the handle arm above always returns. Written out rather than left to
|
||||
// fall off the end so the function still has a return on every path a compiler sees.
|
||||
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *it->second;
|
||||
}
|
||||
|
||||
VertexInputStateBuilder builder;
|
||||
Vector<SizeT> bindingBufferKeys;
|
||||
Vector<SizeT> bindingBaseOffsets;
|
||||
Vector<Uint32> bindingAttributeLocations;
|
||||
Vector<Bool> bindingUsesClientMemory;
|
||||
Vector<VertexStreamConversion> bindingConversions;
|
||||
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
|
||||
Uint32 unsupportedAttribMask = 0;
|
||||
|
||||
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
||||
const auto& attr = vao.GetAttribute(location);
|
||||
if (!attr.Enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
VkFormat sourceVkFormat =
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||
// Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is
|
||||
// load-bearing rather than belt-and-braces: the narrowing is only correct because the
|
||||
// shader's `dvec` input is a `vec` by the time the pipeline is built, and what
|
||||
// guarantees that is the flag being clear. It is clear on every backend today, and a
|
||||
// program with a 64-bit float vertex input is demoted WHOLE for the same reason even
|
||||
// where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag
|
||||
// set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the
|
||||
// module, so a float32 stream would be fed to a Float64 input.
|
||||
const Bool narrowFloat64Arrays =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (hd, CONTRACT-P5C §3.7): with an active transport the answer is the
|
||||
// SERVER's own backend's - the client caps mirror is client memory (rule E).
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr ||
|
||||
!MG_Remote::Server::ServerLoopInstance().Backend()
|
||||
->GetDynamicParameters()
|
||||
.SupportsFloat64VertexAttributes)
|
||||
:
|
||||
#endif
|
||||
MG_Backend::pActiveBackendObject == nullptr ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes;
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) {
|
||||
// No native 64-bit fetch here (see ToVkVertexFormat's Float64 case), but the
|
||||
// source bytes are ordinary IEEE-754 doubles and DemoteFloat64Pass has already
|
||||
// narrowed every dvec input to a vec, so the array is narrowed to match rather
|
||||
// than dropped. Mirrors what DirectGLES does for the same state.
|
||||
const VkFormat narrowedFormat = ToFloat32VertexFormat(attr.Size);
|
||||
if (narrowedFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(narrowedFormat)) {
|
||||
sourceVkFormat = narrowedFormat;
|
||||
conversion = VertexStreamConversion::Float64ToFloat32;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u is a 64-bit (GL_DOUBLE) array; fetching it at "
|
||||
"float32 precision through format=%d (size=%d long=%s)",
|
||||
location, static_cast<Int>(narrowedFormat), attr.Size, attr.IsLong ? "true" : "false");
|
||||
}
|
||||
}
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
"enabled but cannot be mapped to a VkFormat",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
continue;
|
||||
}
|
||||
|
||||
VkFormat vkFormat = sourceVkFormat;
|
||||
if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) {
|
||||
if (IsScaledIntegerVertexFormat(vkFormat)) {
|
||||
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
|
||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||
vkFormat = fallbackFormat;
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
||||
"(type=%s size=%d normalized=%s integer=%s)",
|
||||
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size,
|
||||
attr.Normalized ? "true" : "false", attr.IsInteger ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
if (conversion == VertexStreamConversion::None) {
|
||||
MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
||||
location, static_cast<Int>(sourceVkFormat),
|
||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||
if (attribByteSize == 0) {
|
||||
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
"enabled but cannot be sized",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verbatim, zero included. The frontend already resolved a pointer call's
|
||||
// "tightly packed" stride 0 into the element size (see VertexAttribute::Stride),
|
||||
// so a zero here is the binding model's stride 0 - every vertex reads the same
|
||||
// element - which is exactly what a zero VkVertexInputBindingDescription::stride
|
||||
// means. Substituting the element size fetched a fresh element per vertex and ran
|
||||
// off the end of the buffer (KHR-GL43.vertex_attrib_binding.basic-input-case7/8).
|
||||
// Client-memory arrays cannot reach zero: they only exist on the pointer path.
|
||||
const Uint32 sourceStride = static_cast<Uint32>(attr.Stride);
|
||||
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
||||
attr.Type == DataType::Uint2101010Rev;
|
||||
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
|
||||
// For a client-memory array attr.Offset holds the raw client pointer, and the
|
||||
// draw path re-uploads the data to a 16-aligned transient slice with attribute
|
||||
// offset 0, so only the stride can violate Vulkan's fetch alignment there.
|
||||
const Bool clientMemoryAttribute = attr.Buffer == nullptr;
|
||||
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
|
||||
((sourceStride % requiredAlignment) != 0 ||
|
||||
(!clientMemoryAttribute && (attr.Offset % requiredAlignment) != 0))) {
|
||||
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
|
||||
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||
// attribute into a tightly packed transient stream without changing its format.
|
||||
conversion = VertexStreamConversion::Repack;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
||||
location, attr.Offset, sourceStride, requiredAlignment);
|
||||
}
|
||||
|
||||
Uint32 stride = sourceStride;
|
||||
// A converted stream is tightly packed, so its stride is the converted element
|
||||
// size - unless the source stride is zero, which does not describe a packing at
|
||||
// all but "never advance". That survives the conversion unchanged: the draw path
|
||||
// converts exactly one element and every vertex reads it.
|
||||
if (sourceStride != 0) {
|
||||
if (conversion == VertexStreamConversion::Repack) {
|
||||
stride = static_cast<Uint32>(attribByteSize);
|
||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32 ||
|
||||
conversion == VertexStreamConversion::Float64ToFloat32) {
|
||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||
}
|
||||
}
|
||||
const VkVertexInputRate inputRate =
|
||||
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
|
||||
|
||||
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
||||
const Uint32 binding = static_cast<Uint32>(bindingBufferKeys.size());
|
||||
bindingBufferKeys.push_back(bufferKey);
|
||||
bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0);
|
||||
bindingAttributeLocations.push_back(location);
|
||||
bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
|
||||
bindingConversions.push_back(conversion);
|
||||
builder.AddBinding(binding, stride, inputRate);
|
||||
builder.AddAttribute(location, binding, vkFormat, 0);
|
||||
// Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything
|
||||
// else needs the extension to say it.
|
||||
if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) {
|
||||
bindingDivisors.push_back({binding, static_cast<Uint32>(attr.Divisor)});
|
||||
}
|
||||
}
|
||||
|
||||
const auto& state = builder.Build();
|
||||
|
||||
auto& slot = m_cache[hash];
|
||||
if (!slot) {
|
||||
slot = MakeUnique<BackendVertexInputState>();
|
||||
}
|
||||
BackendVertexInputState& entry = *slot;
|
||||
entry.hash = hash;
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.bindingDivisors = Move(bindingDivisors);
|
||||
entry.bindings = builder.GetBindings();
|
||||
entry.attributes = builder.GetAttributes();
|
||||
// See the layoutHash declaration: hash only the resolved layout, never
|
||||
// buffer identities, so identical layouts across VAOs/buffers agree.
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
|
||||
for (const auto& binding : entry.bindings) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
|
||||
}
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
||||
}
|
||||
for (const auto& divisor : entry.bindingDivisors) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState);
|
||||
entry.attributeLocationMask = 0;
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
if (attribute.location < 32u) {
|
||||
entry.attributeLocationMask |= (1u << attribute.location);
|
||||
}
|
||||
}
|
||||
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
|
||||
entry.bindingConversions = std::move(bindingConversions);
|
||||
entry.unsupportedAttribMask = unsupportedAttribMask;
|
||||
entry.state = state;
|
||||
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
|
||||
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
|
||||
if (!entry.bindingDivisors.empty()) {
|
||||
entry.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(entry.bindingDivisors.size());
|
||||
entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data();
|
||||
entry.state.pNext = &entry.divisorState;
|
||||
} else {
|
||||
entry.state.pNext = nullptr;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
|
||||
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const {
|
||||
out = BackendVertexInputState{};
|
||||
@@ -532,37 +156,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
void VertexInputStateFactory::OnFrameBoundary() {
|
||||
++m_frameBoundaryCounter;
|
||||
|
||||
// Sweep occasionally; evict entries whose last hit is far in the past.
|
||||
// Erasure happens only here, never mid-frame: the draw path holds a
|
||||
// reference into the current entry across its setup, and unordered_map
|
||||
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
|
||||
// proof is needed; an evicted entry that is used again is simply rebuilt
|
||||
// from the VAO state (same hash, same content).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
it = m_cache.erase(it);
|
||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||
// address may be reused by a future insert. Advance through the
|
||||
// process-wide source so the value stays unique across factory
|
||||
// instances (see the member comment). With no legacy arm the memos
|
||||
// live in this factory and die with it, so a per-instance bump is
|
||||
// enough - P2 D12.5.
|
||||
++m_evictionEpoch;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
||||
Bool isBgra, Bool isLong) {
|
||||
if (isBgra) {
|
||||
|
||||
@@ -76,45 +76,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
};
|
||||
|
||||
// The mint is the RENDERER's (MagmaPipeIdentityTables), not a process-global and not
|
||||
// this factory's: VulkanRenderer::LookupVaoDrawMemo has to derive the same {slot, gen}
|
||||
// for the same VAO, and a table that outlived the context it was minted for would share
|
||||
// one reclamation clock across two live contexts (review v2 minor 4).
|
||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice,
|
||||
MagmaPipeIdentityTables& identity):
|
||||
m_config(config), m_physicalDevice(physicalDevice), m_identity(&identity) {}
|
||||
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;
|
||||
// The VAO's content hash IF it has already been memoized, without computing one.
|
||||
// P2 D12.5: the three draw-path readers that used to ask the VAO object this
|
||||
// question ask the factory instead, because that is where the memo lives once the
|
||||
// frontend object stops carrying the backend's state.
|
||||
Bool TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) 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
|
||||
@@ -133,76 +99,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||
|
||||
// ---- P2 D12.5: the backend's memos, off the frontend VAO and into the backend ----
|
||||
//
|
||||
// The two facts that used to live as `mutable` fields on VertexArrayObject
|
||||
// (Get/SetBackendHashMemo and Get/SetBackendStateMemo), kept here instead, keyed on
|
||||
// the VAO's {slot, gen} and guarded by exactly the same config version. A frontend
|
||||
// state object holding the backend's raw pointer is what P2 retires: under split the
|
||||
// backend is in another process and its cache entry has no address a client could
|
||||
// store, so the memo has to live on the side that owns the pointee.
|
||||
//
|
||||
// The AUX memo is not carried over: its two words moved into VaoDrawMemo::layoutHash
|
||||
// and layoutAuxMasks long ago and its getter has no live reader anywhere in the tree,
|
||||
// so the handle arm simply stops writing it (D12.5 says delete rather than move).
|
||||
struct VaoBackendMemos {
|
||||
// Whose memos these are. The identity table can recycle a slot for a different
|
||||
// VAO under LRU pressure, and the handle compare - Gen included - is what says
|
||||
// the contents are this object's and not its predecessor's.
|
||||
MG_Pipe::MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle;
|
||||
Uint64 Hash = 0;
|
||||
Uint32 HashConfigVersion = ~0u;
|
||||
const void* State = nullptr;
|
||||
Uint64 StateEpoch = 0;
|
||||
Uint32 StateConfigVersion = ~0u;
|
||||
};
|
||||
// Grow-on-demand (D12.4), one entry per slot the renderer's mint has ever handed
|
||||
// out, and NO CAPACITY: these two memos had none before this package either - they
|
||||
// were unbounded mutable fields on the VertexArrayObject itself - and re-introducing
|
||||
// eviction here is what review v2 rejected. MagmaPipeSlotTable grows in chunks so an
|
||||
// entry reference stays valid across the nested GetOrCreateVertexInputState call.
|
||||
// 48 B per live VAO, reclaimed with the slot when the object goes idle.
|
||||
mutable MagmaPipeSlotTable<VaoBackendMemos> m_vaoMemos;
|
||||
// The renderer's {slot, gen} mint (see the constructor). Never null under push.
|
||||
MagmaPipeIdentityTables* m_identity = nullptr;
|
||||
// The entry belonging to `vao`, claimed (and cleared) if the slot currently holds
|
||||
// someone else's.
|
||||
VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
// Physical-device format capabilities never change during this factory's
|
||||
// lifetime. The wire path rebuilds layouts without the legacy VAO memo.
|
||||
mutable UnorderedMap<VkFormat, Bool> m_wireVertexFormatSupport;
|
||||
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
|
||||
// invalidates references to stored values - and so does ERASE, which shifts
|
||||
// the rest of the probe cluster into the hole and therefore moves entries
|
||||
// other than the erased one. The draw path (and the VAOs' state-pointer
|
||||
// memos) hold entry pointers across both; 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.
|
||||
//
|
||||
// Drawn from a process-wide source, never a per-instance counter: the VAO
|
||||
// memos outlive this factory (they live on the frontend context's VAOs, the renderer
|
||||
// is destroyed and recreated on EGL surface release/re-create), so a fresh
|
||||
// factory restarting at a dead factory's epoch value would honor its
|
||||
// dangling entry pointers. The constructor takes a value strictly greater
|
||||
// than anything a predecessor ever stamped, so a dead factory's memo can
|
||||
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
||||
// Single-threaded like the rest of the factory (renderer-thread only).
|
||||
//
|
||||
// P2 D12.5: the process-wide source is the LEGACY arm's need. It exists because the
|
||||
// memos live on the frontend VAOs and therefore outlive the factory. The handle arm's
|
||||
// memo table is owned by this factory and dies with it, so a per-instance counter is
|
||||
// enough there and the epoch shrinks back to what it looks like it should be.
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -185,19 +185,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return static_cast<Uint32>(target) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
|
||||
}
|
||||
|
||||
static Bool IsValidSampledImageLayout(VkImageLayout layout) {
|
||||
switch (layout) {
|
||||
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_GENERAL:
|
||||
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static VkImageLayout ResolveSampledReadOnlyLayout(VkImageAspectFlags aspectMask) {
|
||||
return (aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
|
||||
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
|
||||
@@ -800,16 +787,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
void VkTextureManager::BeginDrawSyncScope() {
|
||||
m_drawSyncedThisDraw.clear();
|
||||
m_drawSyncScopeActive = true;
|
||||
}
|
||||
|
||||
void VkTextureManager::EndDrawSyncScope() {
|
||||
m_drawSyncScopeActive = false;
|
||||
m_drawSyncedThisDraw.clear();
|
||||
}
|
||||
|
||||
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& textureOrView) {
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE");
|
||||
|
||||
@@ -825,23 +802,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
|
||||
// Per-draw memo fast path (see BeginDrawSyncScope): a texture already fully
|
||||
// synced earlier in this draw cannot have changed since (no GL mutation runs
|
||||
// mid-SetupDraw), so skip the heavy SyncTexture work and hand back the
|
||||
// already-synced resource. Layout lives on the resource and is updated by the
|
||||
// transition path, so the short-circuited resource still reflects the truth.
|
||||
const Bool memoActive = m_drawSyncScopeActive;
|
||||
if (memoActive) {
|
||||
for (const DrawSyncedTexture& synced : m_drawSyncedThisDraw) {
|
||||
if (synced.identity == identity) {
|
||||
if (synced.resource != nullptr && synced.resource->image != VK_NULL_HANDLE) {
|
||||
return synced.resource;
|
||||
}
|
||||
break; // resource unexpectedly gone -> fall through to a full sync
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-draw memo probe (see SyncedTextureMemoEntry): skips both map
|
||||
// lookups and the (re)registration path for repeat-bound textures.
|
||||
TextureResource* resourcePtr = nullptr;
|
||||
@@ -903,19 +863,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (memoActive) {
|
||||
Bool recorded = false;
|
||||
for (const DrawSyncedTexture& synced : m_drawSyncedThisDraw) {
|
||||
if (synced.identity == identity) {
|
||||
recorded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!recorded) {
|
||||
m_drawSyncedThisDraw.push_back({identity, resourcePtr});
|
||||
}
|
||||
}
|
||||
|
||||
return resourcePtr;
|
||||
}
|
||||
|
||||
@@ -1051,52 +998,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return attachmentView;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture,
|
||||
Uint32 mipLevel) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// As in GetOrCreateViewAtMipLevel: perMipSampledViews belongs to the storage texture's
|
||||
// own format and aspect, so a GL view has to go to the keyed cache.
|
||||
if (texture.IsTextureView()) {
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
|
||||
// Storage space already (see ToStorageMipLevel); only the level COUNT narrows.
|
||||
window.baseMipLevel = mipLevel;
|
||||
window.levelCount = 1;
|
||||
return GetOrCreateWindowedSampledView(texture, *resource, window);
|
||||
}
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
if (resource->perMipSampledViews.size() != resource->mipLevels) {
|
||||
resource->perMipSampledViews.resize(resource->mipLevels, VK_NULL_HANDLE);
|
||||
}
|
||||
|
||||
VkImageView& perMipSampledView = resource->perMipSampledViews[mipLevel];
|
||||
if (perMipSampledView != VK_NULL_HANDLE) {
|
||||
return perMipSampledView;
|
||||
}
|
||||
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||
const VkImageAspectFlags sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(resource->aspect, texture.GetDepthStencilTextureMode());
|
||||
perMipSampledView = CreateImageView(resource->image, resource->format, sampledAspect, resource->viewType,
|
||||
mipLevel, 1, 0, resource->arrayLayers, &sampledComponents);
|
||||
if (perMipSampledView == VK_NULL_HANDLE) {
|
||||
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u", __func__, texture.GetExternalIndex(),
|
||||
mipLevel);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
return perMipSampledView;
|
||||
}
|
||||
|
||||
// Builds (and caches) one sampled VkImageView over `resource`'s image for an arbitrary
|
||||
// window - the shared back end of every GL-texture-view sampled path. Keyed by the whole
|
||||
// window, which is what keeps a D24S8's depth-aspect view and its stencil-aspect view apart
|
||||
@@ -1230,121 +1131,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return view;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture,
|
||||
Uint32 mipLevel, VkFormat format,
|
||||
Bool layered, Int32 layer) {
|
||||
// mipLevel and layer arrive in STORAGE space; ResolveStorageImageDescriptor converts
|
||||
// the glBindImageTexture values with ToStorageMipLevel / ToStorageArrayLayer.
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels ||
|
||||
resource->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
||||
(resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
if (format == VK_FORMAT_UNDEFINED) {
|
||||
format = resource->format;
|
||||
}
|
||||
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
|
||||
MGLOG_E_ONCE("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Int>(resource->format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if (format != resource->format &&
|
||||
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
// A GL texture view opens onto a WINDOW of the storage's layers; a layered image
|
||||
// binding of it must not reach past that window into the parent's other layers.
|
||||
Uint32 baseArrayLayer = ToStorageArrayLayer(&texture, 0);
|
||||
Uint32 layerCount = texture.IsTextureView()
|
||||
? std::min(static_cast<Uint32>(texture.GetViewNumLayers()),
|
||||
resource->arrayLayers - baseArrayLayer)
|
||||
: resource->arrayLayers;
|
||||
VkImageViewType viewType = resource->viewType;
|
||||
if (!layered) {
|
||||
switch (resource->viewType) {
|
||||
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
|
||||
viewType = VK_IMAGE_VIEW_TYPE_1D;
|
||||
break;
|
||||
case VK_IMAGE_VIEW_TYPE_2D_ARRAY:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
break;
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
MGLOG_E_ONCE("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
||||
__func__, texture.GetExternalIndex());
|
||||
return VK_NULL_HANDLE;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (viewType != resource->viewType) {
|
||||
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
|
||||
MGLOG_E_ONCE("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
||||
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
baseArrayLayer = static_cast<Uint32>(layer);
|
||||
layerCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||
viewType == resource->viewType;
|
||||
if (format == resource->format && isFullResourceView && !texture.IsTextureView()) {
|
||||
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||
}
|
||||
|
||||
const TextureResource::StorageImageViewKey key{
|
||||
.mipLevel = mipLevel,
|
||||
.baseArrayLayer = baseArrayLayer,
|
||||
.layerCount = layerCount,
|
||||
.viewType = viewType,
|
||||
.format = format,
|
||||
};
|
||||
auto it = resource->storageImageViews.find(key);
|
||||
if (it != resource->storageImageViews.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
VkFormatFeatureFlags requiredFormatFeatures = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT;
|
||||
if (format != resource->format &&
|
||||
(format == VK_FORMAT_R32_UINT || format == VK_FORMAT_R32_SINT)) {
|
||||
requiredFormatFeatures |= VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT;
|
||||
}
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
|
||||
MGLOG_E_ONCE("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
||||
"(available=0x%x)",
|
||||
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
|
||||
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
const VkImageView view = CreateImageView(resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, viewType,
|
||||
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
|
||||
VK_IMAGE_USAGE_STORAGE_BIT);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
resource->storageImageViews.emplace(key, view);
|
||||
MGLOG_D("%s: created storage image view textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return view;
|
||||
}
|
||||
|
||||
void VkTextureManager::StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return;
|
||||
@@ -1391,16 +1177,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
|
||||
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||
MOBILEGL_ASSERT(it != m_textureResources.end(),
|
||||
"UpdateTrackedImageLayout: textureId=%d has no tracked resource", texture->GetExternalIndex());
|
||||
MOBILEGL_ASSERT(it->second.image != VK_NULL_HANDLE,
|
||||
"UpdateTrackedImageLayout: textureId=%d has null image", texture->GetExternalIndex());
|
||||
it->second.layout = newLayout;
|
||||
}
|
||||
|
||||
void VkTextureManager::UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject* texture,
|
||||
Uint32 writtenMipLevel,
|
||||
@@ -1457,297 +1233,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.layout = newLayout;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (IsValidSampledImageLayout(resource->layout)) {
|
||||
return true;
|
||||
}
|
||||
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
MGLOG_W_ONCE("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
|
||||
texture.GetExternalIndex());
|
||||
}
|
||||
|
||||
VkImageLayout targetLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
|
||||
MOBILEGL_ASSERT(resource->layout == VK_IMAGE_LAYOUT_UNDEFINED ||
|
||||
resource->layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
||||
"TransitionTextureForSampling: unsupported color layout=%d for textureId=%d",
|
||||
static_cast<Int>(resource->layout), texture.GetExternalIndex());
|
||||
if (resource->layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
|
||||
srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
}
|
||||
targetLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
} else if ((resource->aspect & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0) {
|
||||
MOBILEGL_ASSERT(resource->layout == VK_IMAGE_LAYOUT_UNDEFINED ||
|
||||
resource->layout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
||||
"TransitionTextureForSampling: unsupported depth/stencil layout=%d for textureId=%d",
|
||||
static_cast<Int>(resource->layout), texture.GetExternalIndex());
|
||||
if (resource->layout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
|
||||
srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
||||
srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||
}
|
||||
targetLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
|
||||
} else {
|
||||
MOBILEGL_ASSERT(false, "TransitionTextureForSampling: unsupported aspect mask=0x%x for textureId=%d",
|
||||
static_cast<Uint32>(resource->aspect), texture.GetExternalIndex());
|
||||
}
|
||||
|
||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||
s_sampledReadStagesSlot->value, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
return ok;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::TransitionTextureForStorageImage(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) {
|
||||
MGLOG_D("TransitionTextureForStorageImage: multisample textureId=%d is not exposed as a storage image",
|
||||
texture.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
if (resource->layout == VK_IMAGE_LAYOUT_GENERAL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
|
||||
VkAccessFlags srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT;
|
||||
GetImageTransitionSourceState(resource->layout, srcStageMask, srcAccessMask);
|
||||
|
||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout,
|
||||
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||
resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded, and an
|
||||
// image unit may store to it.
|
||||
StampResourceRecordingWrite(*resource);
|
||||
return ok;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::SnapshotTextureForSampling(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
SamplerNumericDomain numericDomain,
|
||||
VkPipelineStageFlags consumerShaderStageMask,
|
||||
SampledTextureSnapshot& outSnapshot) {
|
||||
outSnapshot = {};
|
||||
TextureResource* source = SyncTextureAndGetDescriptor(texture);
|
||||
if (source == nullptr || source->image == VK_NULL_HANDLE || source->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
||||
source->sampledLevelCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const VkFormat sampledFormat = ResolveSampledImageViewFormat(source->format, numericDomain);
|
||||
if (sampledFormat == VK_FORMAT_UNDEFINED ||
|
||||
!AreSampledImageViewFormatsCompatible(source->format, sampledFormat)) {
|
||||
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d cannot create sampled view format=%d from image format=%d",
|
||||
texture.GetExternalIndex(), static_cast<Int>(sampledFormat), static_cast<Int>(source->format));
|
||||
return false;
|
||||
}
|
||||
if (sampledFormat != source->format &&
|
||||
(source->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d needs unavailable mutable image format=%d for sampled view=%d",
|
||||
texture.GetExternalIndex(), static_cast<Int>(source->format), static_cast<Int>(sampledFormat));
|
||||
return false;
|
||||
}
|
||||
|
||||
VkImageType imageType = VK_IMAGE_TYPE_2D;
|
||||
switch (source->viewType) {
|
||||
case VK_IMAGE_VIEW_TYPE_1D:
|
||||
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
|
||||
imageType = VK_IMAGE_TYPE_1D;
|
||||
break;
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
imageType = VK_IMAGE_TYPE_3D;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
TextureResource snapshot{};
|
||||
VkImageCreateInfo imageInfo{};
|
||||
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
imageInfo.flags = source->imageCreateFlags;
|
||||
imageInfo.imageType = imageType;
|
||||
imageInfo.extent = {source->extent.width, source->extent.height, source->depth};
|
||||
imageInfo.mipLevels = source->mipLevels;
|
||||
imageInfo.arrayLayers = source->arrayLayers;
|
||||
imageInfo.format = source->format;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
|
||||
// Keep the temporary's view-format list just as narrow as the source's sampler use. This
|
||||
// has no storage-image usage, so unlike an app image binding the exact list is knowable.
|
||||
Vector<VkFormat> viewFormats;
|
||||
VkImageFormatListCreateInfo formatListInfo{};
|
||||
if (m_imageFormatListSupported && (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||
viewFormats.push_back(source->format);
|
||||
if (sampledFormat != source->format) {
|
||||
viewFormats.push_back(sampledFormat);
|
||||
}
|
||||
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
||||
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
||||
formatListInfo.pViewFormats = viewFormats.data();
|
||||
imageInfo.pNext = &formatListInfo;
|
||||
}
|
||||
|
||||
VmaAllocationCreateInfo allocationInfo{};
|
||||
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||
const VkResult createResult =
|
||||
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &snapshot.image, &snapshot.allocation, nullptr);
|
||||
if (createResult != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("SnapshotTextureForSampling: vmaCreateImage failed result=%d textureId=%d", createResult,
|
||||
texture.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot.extent = source->extent;
|
||||
snapshot.depth = source->depth;
|
||||
snapshot.arrayLayers = source->arrayLayers;
|
||||
snapshot.mipLevels = source->mipLevels;
|
||||
snapshot.sampledBaseMipLevel = source->sampledBaseMipLevel;
|
||||
snapshot.sampledLevelCount = source->sampledLevelCount;
|
||||
snapshot.format = source->format;
|
||||
snapshot.aspect = source->aspect;
|
||||
snapshot.viewType = source->viewType;
|
||||
snapshot.sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
snapshot.imageCreateFlags = imageInfo.flags;
|
||||
snapshot.usageFlags = imageInfo.usage;
|
||||
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||
const VkImageAspectFlags sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(snapshot.aspect, texture.GetDepthStencilTextureMode());
|
||||
snapshot.sampledView = CreateImageView(snapshot.image, sampledFormat, sampledAspect, snapshot.viewType,
|
||||
snapshot.sampledBaseMipLevel, snapshot.sampledLevelCount, 0,
|
||||
snapshot.arrayLayers, &sampledComponents);
|
||||
if (snapshot.sampledView == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("SnapshotTextureForSampling: failed to create sampled view textureId=%d", texture.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
VkPipelineStageFlags sourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags sourceAccessMask = 0;
|
||||
const VkImageLayout sourceLayout = source->layout;
|
||||
GetImageTransitionSourceState(sourceLayout, sourceStageMask, sourceAccessMask);
|
||||
if (!TransitionImageLayout(commandBuffer, source->image, source->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
sourceStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, sourceAccessMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, source->aspect, 0, source->mipLevels) ||
|
||||
!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
|
||||
snapshot.sampledLevelCount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<VkImageCopy> copyRegions;
|
||||
copyRegions.reserve(snapshot.sampledLevelCount);
|
||||
for (Uint32 level = snapshot.sampledBaseMipLevel;
|
||||
level < snapshot.sampledBaseMipLevel + snapshot.sampledLevelCount; ++level) {
|
||||
VkImageCopy copy{};
|
||||
copy.srcSubresource = {source->aspect, level, 0, source->arrayLayers};
|
||||
copy.dstSubresource = {snapshot.aspect, level, 0, snapshot.arrayLayers};
|
||||
copy.extent = {std::max(source->extent.width >> level, 1u),
|
||||
std::max(source->extent.height >> level, 1u),
|
||||
std::max(source->depth >> level, 1u)};
|
||||
copyRegions.push_back(copy);
|
||||
}
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<Uint32>(copyRegions.size()), copyRegions.data());
|
||||
|
||||
if (!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout,
|
||||
ResolveSampledReadOnlyLayout(snapshot.aspect), VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
consumerShaderStageMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
|
||||
snapshot.sampledLevelCount) ||
|
||||
!TransitionImageLayout(commandBuffer, source->image, source->layout, sourceLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, consumerShaderStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||
source->aspect, 0, source->mipLevels)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
StampResourceRecordingUse(*source);
|
||||
outSnapshot = {.imageView = snapshot.sampledView, .layout = snapshot.layout};
|
||||
DeferResourceRelease(Move(snapshot));
|
||||
return true;
|
||||
}
|
||||
|
||||
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
||||
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
||||
}
|
||||
|
||||
Bool VkTextureManager::NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const {
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
if (m_storageImageTextures.find(identity) == m_storageImageTextures.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto it = m_textureResources.find(identity);
|
||||
// No image yet: the first sync creates it with STORAGE straight away, so there is nothing
|
||||
// to preserve and nothing to order against.
|
||||
return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE &&
|
||||
!it->second.storageUsageResolved;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const {
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
const auto it = m_textureResources.find(identity);
|
||||
// No image yet: the first sync sizes the chain from the levels the texture already
|
||||
// defines, so nothing is recreated and there is nothing to order against.
|
||||
if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
const TextureResource& resource = it->second;
|
||||
const IntVec3 extent = {static_cast<Int>(resource.extent.width), static_cast<Int>(resource.extent.height),
|
||||
static_cast<Int>(resource.depth)};
|
||||
return resource.mipLevels < ComputeFullMipLevelCount(extent);
|
||||
}
|
||||
|
||||
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
const auto it = m_textureResources.find(identity);
|
||||
if (it == m_textureResources.end()) {
|
||||
return true;
|
||||
}
|
||||
const TextureResource& resource = it->second;
|
||||
if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
|
||||
return true;
|
||||
}
|
||||
// The image predates this texture's first image-unit binding, so it was created without
|
||||
// STORAGE usage and has to be recreated - which is illegal inside a render pass.
|
||||
if (!resource.storageUsageResolved &&
|
||||
m_storageImageTextures.find(identity) != m_storageImageTextures.end()) {
|
||||
return true;
|
||||
}
|
||||
// Mirror SyncTexture's cross-draw skip condition: any version drift means the sync
|
||||
// path may upload or rebuild, both of which need the render pass ended first.
|
||||
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
|
||||
return resource.syncedContentVersion != texture.GetContentVersion() ||
|
||||
resource.syncedShapeVersion != texture.GetShapeVersion() ||
|
||||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
|
||||
resource.syncedMipLevelCount != mipLevelCount;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image,
|
||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||
@@ -1804,12 +1289,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
SizeT VkTextureManager::PruneDeadTextures() {
|
||||
// Erasing entries would dangle the raw TextureResource pointers memoized for the
|
||||
// current draw; every call path (BeginFrame, and CollectGarbage at the top of a
|
||||
// freshly opened draw-sync scope) runs before any memo entry is recorded.
|
||||
MOBILEGL_ASSERT(m_drawSyncedThisDraw.empty(),
|
||||
"PruneDeadTextures: draw-sync memo holds raw resource pointers an erase would dangle");
|
||||
|
||||
Vector<MG_State::GLState::ITextureObject*> expiredTextures;
|
||||
expiredTextures.reserve(m_aliveObjects.size());
|
||||
for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) {
|
||||
@@ -5069,11 +4548,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat);
|
||||
}
|
||||
|
||||
Bool VkTextureManager::AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) {
|
||||
if (imageFormat == viewFormat) {
|
||||
return true;
|
||||
}
|
||||
return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) &&
|
||||
vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat);
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -668,25 +668,11 @@ public:
|
||||
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);
|
||||
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
|
||||
// restored to its prior layout, so image-store descriptors continue to name the original image.
|
||||
// The transient ownership is tied to the current frame slot and is safe through its submission.
|
||||
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
|
||||
SamplerNumericDomain numericDomain,
|
||||
VkPipelineStageFlags consumerShaderStageMask,
|
||||
SampledTextureSnapshot& outSnapshot);
|
||||
|
||||
// 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
|
||||
@@ -721,27 +707,6 @@ public:
|
||||
Bool WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
// The live resource, or null.
|
||||
TextureResource* FindTextureResource(MG_State::GLState::ITextureObject& textureOrView);
|
||||
// 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;
|
||||
|
||||
// `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides
|
||||
// anything for an image that carries both aspects. Defaulted so the call sites that have no
|
||||
// texture in hand keep the depth-aspect answer they have always given.
|
||||
@@ -749,8 +714,6 @@ public:
|
||||
GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT);
|
||||
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
|
||||
// Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`.
|
||||
//
|
||||
// The barrier covers EVERY array layer of the image, and there is deliberately no layer
|
||||
@@ -772,29 +735,6 @@ public:
|
||||
|
||||
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;
|
||||
@@ -878,20 +818,8 @@ private:
|
||||
// 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;
|
||||
// SyncWireTextureShape defining the RGBA8 storage a YUV shared image is converted into.
|
||||
Bool m_syncingYuvTextureStorage = 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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -243,14 +243,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||
const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView = nullptr);
|
||||
// ANGLE-style consecutive-draw fast path: SetupDraw snapshots the fully
|
||||
// resolved draw configuration; the next draw whose cheap version/identity
|
||||
// checks all match skips the resolution half (LOD probe, sampled-set
|
||||
// walk, render-pass and pipeline resolution) and jumps straight to the
|
||||
// per-draw tail. Returns false (leaving no side effects that the full
|
||||
// path cannot redo idempotently) whenever anything might have changed.
|
||||
Bool TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||
const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView);
|
||||
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
|
||||
const RenderPassEntry& compatibleRenderPassEntry);
|
||||
|
||||
@@ -295,11 +287,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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);
|
||||
// GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the
|
||||
// read framebuffer's depth/stencil attachment (per-aspect buffer copies with
|
||||
// CPU repacking into the requested client layout).
|
||||
void ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
||||
// expects command recording to be active and any render pass already ended.
|
||||
//
|
||||
@@ -363,11 +350,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DrawElements(const DrawIndexedCmd& payload);
|
||||
void MultiDrawArrays(const MultiDrawCmd& payload);
|
||||
void MultiDrawElements(const MultiDrawIndexedCmd& payloads);
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
|
||||
GLsizei stride);
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride);
|
||||
void Present();
|
||||
|
||||
const PhysicalDevice& GetPhysicalDevice() const;
|
||||
@@ -1544,432 +1526,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DepthMipmapResources m_depthMipmapResources;
|
||||
Vector<DeferredDepthMipmapCleanup> m_deferredDepthMipmapCleanup;
|
||||
|
||||
// Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
|
||||
// texture SET is provably unchanged from the previous draw: same program (lifetime id +
|
||||
// backend-state version, which covers sampler-uniform reassignment / relink) and transform
|
||||
// flags, no texture bind/unbind/delete since (GetTextureBindGeneration), and nothing that
|
||||
// moves a texture's shape or a sampler's parameters since (GetSamplingResolutionGeneration
|
||||
// - membership depends on mipmap-completeness, which both of those decide). On a hit,
|
||||
// m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
|
||||
// layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
|
||||
// walk is skipped. The program lifetime id (never reused, unlike the GL name) and the
|
||||
// monotonic bind generation make the key ABA-proof; the per-command-buffer reset is a cheap
|
||||
// belt-and-suspenders.
|
||||
Bool m_lastSampledSetValid = false;
|
||||
Uint64 m_lastSampledSetProgramLifetimeId = 0;
|
||||
Uint32 m_lastSampledSetProgramVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
||||
Uint64 m_lastSampledSetBindGeneration = 0;
|
||||
Uint64 m_lastSampledSetSamplingGeneration = 0;
|
||||
// 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;
|
||||
|
||||
// Memo for the per-draw explicit-LOD-0 eligibility probe
|
||||
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
|
||||
// sampled-set memo, plus the sampled textures' params-version sum so a
|
||||
// level-range or filter change re-probes. On a hit the resolved
|
||||
// transform flags are reused, which also collapses the two
|
||||
// GetOrCreateProgram lookups into one.
|
||||
Bool m_lastLodDecisionValid = false;
|
||||
Uint64 m_lastLodProgramLifetimeId = 0;
|
||||
Uint32 m_lastLodProgramVersion = 0;
|
||||
Uint64 m_lastLodBindGeneration = 0;
|
||||
Uint64 m_lastLodParamsSum = 0;
|
||||
// Sampling-resolution generation at probe time. The probe reads the effective
|
||||
// sampler's filters/aniso/LOD range, whose setters bump only this counter -
|
||||
// the params-version sum above never moves for them.
|
||||
Uint64 m_lastLodSamplingGeneration = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||
|
||||
// Does the current program's vertex stage declare the BaseVertex builtin? A property
|
||||
// of the program's SPIR-V, so (lifetime id, backend-state version) is the whole key.
|
||||
//
|
||||
// Memoized rather than re-asked because asking means resolving the UN-zeroed program
|
||||
// variant, and a program that only ever draws non-indexed would then compile a variant
|
||||
// no draw uses AND re-stamp its use every draw, so the idle sweep could never retire
|
||||
// it. With the memo the answer is known before the first lookup and only the variant
|
||||
// the draw actually needs is resolved.
|
||||
Bool m_lastBaseVertexQueryValid = false;
|
||||
Uint64 m_lastBaseVertexProgramLifetimeId = 0;
|
||||
Uint32 m_lastBaseVertexProgramVersion = 0;
|
||||
Bool m_lastBaseVertexReads = false;
|
||||
|
||||
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
|
||||
// render-pass caches are open-addressing maps whose entries move on
|
||||
// insert, so no pointers into them are cached; the pipeline handle is
|
||||
// protected by the command-buffer-boundary reset plus the mid-frame
|
||||
// pipeline-destruction resets, and monotonic epochs guard everything
|
||||
// that can be destroyed or recreated between draws.
|
||||
struct SetupDrawSnapshot {
|
||||
Bool valid = false;
|
||||
Uint8 aspects = 0;
|
||||
GLenum mode = 0;
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint32 programVersion = 0;
|
||||
const void* vao = nullptr;
|
||||
// Same rule as VaoDrawMemo::vaoLifetimeId: (address, config version) is not an
|
||||
// identity, because a recycled address can arrive carrying a config version
|
||||
// the dead VAO also had (two mutations to configure one attribute is the
|
||||
// common shape), and "the VAO did not move" would then skip the layout
|
||||
// re-resolve for a different VAO.
|
||||
Uint64 vaoLifetimeId = 0;
|
||||
// P2 D12.4: the handle arm's answer to the same question, and one compare rather
|
||||
// than the pair above. Kept BESIDE them rather than replacing them because the
|
||||
// pre-handle arm is still compiled (MOBILEGL_PIPE_LEGACY_MEMOS) and this snapshot
|
||||
// is a value struct, not a wire type.
|
||||
MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
Uint32 vaoConfigVersion = 0;
|
||||
const void* drawFbo = nullptr;
|
||||
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
|
||||
// deleted FBO recycled at the same address with the same fresh version
|
||||
// count would otherwise compare equal (same ABA as the render-pass
|
||||
// manager's fast-path memo).
|
||||
Uint64 drawFboLifetimeId = 0;
|
||||
Uint16 fboVersion = 0;
|
||||
Bool drawFboIsDefault = false;
|
||||
Uint renderStateVersion = 0;
|
||||
Uint64 bindGeneration = 0;
|
||||
Uint32 baseTransformFlags = 0;
|
||||
Uint32 resolvedTransformFlags = 0;
|
||||
// What ResolvePrimitiveRestartEnable answered for the draw this snapshot was taken
|
||||
// from, i.e. what its pipeline's primitiveRestartEnable was built with. `aspects`
|
||||
// already separates indexed from non-indexed draws, but not one index TYPE from
|
||||
// another, and a restart index that fits GL_UNSIGNED_INT but not GL_UNSIGNED_SHORT
|
||||
// makes those two draws want different pipelines.
|
||||
Bool primitiveRestartEnable = false;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint32 imageIndex = 0;
|
||||
Uint64 textureEraseEpoch = 0;
|
||||
Uint64 textureImageEpoch = 0;
|
||||
Uint64 renderbufferImageEpoch = 0;
|
||||
Uint64 sampledContentSum = 0;
|
||||
Uint64 sampledParamsSum = 0;
|
||||
// Guards the sampler-descriptor reuse hint: bumped by any sampler-object
|
||||
// parameter or texture shape change (see GetSamplingResolutionGeneration),
|
||||
// none of which the sums above cover.
|
||||
Uint64 samplingResolutionGeneration = 0;
|
||||
// Render-pass flavor input (DepthTest || StencilTest at snapshot time).
|
||||
// A pipeline-state change that leaves this equal cannot change which
|
||||
// render pass GetOrCreateRenderPass would pick, so the fast path may
|
||||
// re-resolve just the pipeline against the active pass; a change that
|
||||
// flips it must fall back to the full path's pass selection.
|
||||
Bool drawUsesDepthStencil = false;
|
||||
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
|
||||
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
|
||||
// of which the programLifetimeId/programVersion guards above already pin - carried
|
||||
// here so the fast path does not re-fetch the program object to re-derive it.
|
||||
Uint32 viewportCount = 1;
|
||||
IntVec2 renderPassExtent = {0, 0};
|
||||
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||
// pipeline-state hash input, so the fast path can refresh that hash and
|
||||
// probe the pipeline memo after a state change without re-fetching the
|
||||
// render-pass entry (the pass itself is pinned by renderPassHash above).
|
||||
Uint32 renderPassColorCount = 0;
|
||||
// Pinned with the colour count and for the same reason: the fast path recomputes the
|
||||
// pipeline-state value hash from the snapshot, and that hash reads the sample count.
|
||||
VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
// layoutHash of the snapshotting draw's vertex-input state. The pipeline and
|
||||
// the vertex-input pre-flight depend on the VAO only through this (plus the
|
||||
// program, pinned separately), so a changed VAO whose aux memo carries the
|
||||
// same layoutHash re-uses the snapshot's pipeline and pre-flight verdict
|
||||
// outright - the VAO-cycling case Minecraft chunk rendering hits every draw.
|
||||
Uint64 vaoLayoutHash = 0;
|
||||
// Memoised ProgramFactory entry of the snapshotting draw, valid while
|
||||
// (programLifetimeId, programVersion, resolvedTransformFlags) match - all
|
||||
// checked above - AND the factory's cache structure epoch is unchanged (the
|
||||
// cache is open-addressing and holds entries by value, so any insert/erase
|
||||
// moves them). The fast path must re-stamp use through StampProgramUse when
|
||||
// it bypasses GetOrCreateProgram, or the idle sweep could evict a live entry.
|
||||
const ProgramFactory::VkProgramObject* programObj = nullptr;
|
||||
Uint64 programFactoryEpoch = 0;
|
||||
// Per-entry copies of the snapshotting draw's sampled set (the scratch
|
||||
// vectors below hold only the LAST full-path draw's set, which with more
|
||||
// than one snapshot entry is not necessarily this entry's program).
|
||||
// sampledTextures/sampledResources carry the same epoch-guarded pointer
|
||||
// lifetime rules as the scratch originals: textureEraseEpoch (checked
|
||||
// every probe) declines the entry before any erased resource pointer
|
||||
// could be dereferenced. sampledLayouts is the layout VALUE each
|
||||
// resource held when this entry's descriptors were built (the
|
||||
// descriptor-reuse hint needs the SAME layout, not just a sampleable
|
||||
// one), and sampledBindingRecords feeds SampledBindingsUnchanged when
|
||||
// the bind generation moved.
|
||||
Vector<MG_State::GLState::ITextureObject*> sampledTextures;
|
||||
Vector<VkTextureManager::TextureResource*> sampledResources;
|
||||
Vector<VkImageLayout> sampledLayouts;
|
||||
Vector<UniformManager::SampledBindingRecord> sampledBindingRecords;
|
||||
};
|
||||
// Program-keyed snapshot entries: program ping-pong (Sodium switches programs
|
||||
// mid-frame every few draws) would otherwise evict the single snapshot on
|
||||
// every switch and send every draw through the full path. Entries are found
|
||||
// by programLifetimeId (MRU-first probe); every other guard stays per-probe,
|
||||
// so a stale entry declines itself exactly like the old single snapshot did.
|
||||
static constexpr Uint32 kSetupDrawSnapshotCount = 4;
|
||||
SetupDrawSnapshot m_setupDrawSnapshots[kSetupDrawSnapshotCount];
|
||||
Uint32 m_setupDrawSnapshotMru = 0; // last entry that hit or was filled
|
||||
Uint32 m_setupDrawSnapshotVictim = 0; // round-robin fill cursor when all entries are live
|
||||
void InvalidateSetupDrawSnapshots() {
|
||||
for (auto& snapshot : m_setupDrawSnapshots) {
|
||||
snapshot.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
||||
// draw call and must not allocate.
|
||||
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
||||
// Per-binding (texture, effective sampler) lifetime-id records from the same
|
||||
// CollectSampledTextures walk that filled m_sampledTexturesScratch. The fast
|
||||
// path shadow-compares against them (SampledBindingsUnchanged) when the
|
||||
// texture bind generation moved, so a redundant glBindSampler/glBindTexture
|
||||
// storm that resolves to the same bindings keeps the fast path.
|
||||
Vector<UniformManager::SampledBindingRecord> m_sampledBindingRecordsScratch;
|
||||
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
|
||||
// first sampled-texture loop: the resolved backend resources, so the
|
||||
// post-transition loop can skip re-resolving textures whose layout is
|
||||
// already sampleable.
|
||||
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||
Vector<UniformManager::SamplerImageFeedbackBinding> m_samplerImageFeedbackScratch;
|
||||
Vector<UniformManager::SamplerBindingOverride> m_samplerImageBindingOverridesScratch;
|
||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||
// Per-draw scratch buffer (clear keeps capacity) - this path runs for every draw call and
|
||||
// must not allocate.
|
||||
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
||||
Vector<Float> m_vertexConversionScratch;
|
||||
Vector<Uint8> m_vertexRepackScratch;
|
||||
|
||||
struct ConvertedVertexStreamKey {
|
||||
const MG_State::GLState::BufferObject* buffer = nullptr;
|
||||
Uint64 changeSerial = 0;
|
||||
SizeT baseOffset = 0;
|
||||
Uint32 sourceStride = 0;
|
||||
DataType type = DataType::Float32;
|
||||
Int size = 0;
|
||||
Bool normalized = false;
|
||||
Bool isInteger = false;
|
||||
VertexInputStateFactory::VertexStreamConversion conversion =
|
||||
VertexInputStateFactory::VertexStreamConversion::None;
|
||||
|
||||
Bool operator==(const ConvertedVertexStreamKey& other) const {
|
||||
return buffer == other.buffer && changeSerial == other.changeSerial &&
|
||||
baseOffset == other.baseOffset && sourceStride == other.sourceStride &&
|
||||
type == other.type && size == other.size && normalized == other.normalized &&
|
||||
isInteger == other.isInteger && conversion == other.conversion;
|
||||
}
|
||||
};
|
||||
|
||||
struct ConvertedVertexStreamKeyHash {
|
||||
SizeT operator()(const ConvertedVertexStreamKey& key) const {
|
||||
SizeT hash = std::hash<const void*>{}(key.buffer);
|
||||
auto combine = [&hash](SizeT value) {
|
||||
hash ^= value + static_cast<SizeT>(0x9e3779b97f4a7c15ull) + (hash << 6) + (hash >> 2);
|
||||
};
|
||||
combine(std::hash<Uint64>{}(key.changeSerial));
|
||||
combine(std::hash<SizeT>{}(key.baseOffset));
|
||||
combine(std::hash<Uint32>{}(key.sourceStride));
|
||||
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.type)));
|
||||
combine(std::hash<Int>{}(key.size));
|
||||
combine(std::hash<Bool>{}(key.normalized));
|
||||
combine(std::hash<Bool>{}(key.isInteger));
|
||||
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.conversion)));
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
struct ConvertedVertexStream {
|
||||
BufferSlice slice;
|
||||
// Number of source elements the cached slice covers. A draw needing a prefix of
|
||||
// this range reuses the slice (converted streams are tightly packed); a draw
|
||||
// needing more reconverts and replaces the entry, so per (buffer, layout) a
|
||||
// frame converts at most the largest range any draw asked for.
|
||||
SizeT elementCount = 0;
|
||||
// Pins the source buffer for the frame so its heap address cannot be reused by
|
||||
// a new BufferObject while this pointer-keyed entry is alive.
|
||||
SharedPtr<const MG_State::GLState::BufferObject> sourcePin;
|
||||
};
|
||||
UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
|
||||
m_convertedVertexStreams;
|
||||
|
||||
// One VAO's resolved vkCmdBindVertexBuffers arguments, reusable by a later draw
|
||||
// that would resolve them to the same thing. Consecutive draws in a chunk-renderer
|
||||
// frame keep the program and the vertex layout and only swap the VAO, so a
|
||||
// per-VAO memo turns the second and later draws through each VAO into a validate
|
||||
// plus (usually skipped) rebind.
|
||||
//
|
||||
// Only whole-buffer bindings are memoised. Client-memory and format-converted
|
||||
// streams re-upload from a range that depends on the draw's own vertex/index
|
||||
// range, and synthetic bindings carry glVertexAttrib* values that are not part
|
||||
// of any key here; a layout using any of them is never stored.
|
||||
// Field order is hit-path cache locality, hot to cold: the per-draw validate
|
||||
// reads the scalars and the EBO memo head, then only the first bindingCount
|
||||
// elements of vkBuffers/vkOffsets; the per-binding revalidation arrays at the
|
||||
// tail are touched once per frame at most.
|
||||
struct ResolvedVertexBindings {
|
||||
// Must equal DynamicStateShadow::kMaxShadowedVertexBindings (static_assert in
|
||||
// the .cpp): past that width the bind shadow cannot skip a redundant bind
|
||||
// either, so a wider layout resolves per draw. Minecraft-shaped layouts use four.
|
||||
static constexpr Uint32 kMaxBindings = 8;
|
||||
|
||||
// Frame serial of the last completed resolve OR cross-frame revalidation.
|
||||
// Zero until a resolve completes, and reset to zero before one starts, so a
|
||||
// resolve that bails out midway cannot leave a half-filled entry matchable.
|
||||
// Unlike the original frame-scoped memo, an entry whose buffers are all
|
||||
// resident and unmapped is revalidated across frames (per-binding slice
|
||||
// epoch compares) instead of re-resolved - see TryBindResolvedVertexBindings.
|
||||
Uint64 frameSerial = 0;
|
||||
// Identity of the resolved Vulkan layout: the VAO's content hash
|
||||
// (VertexInputStateFactory::GetOrComputeHash - the same value the factory
|
||||
// keys its entries on) fixes bindings.size(), each binding's base offset,
|
||||
// which bindings are client/converted, and (through the mixed-in buffer
|
||||
// addresses) which buffer each binding reads. Compared against the VAO's
|
||||
// own hash memo on the hit path, so a hit never touches the factory entry.
|
||||
VertexInputStateFactory::HashType vertexInputHash = 0;
|
||||
// The program's vertex input layout: decides the synthetic-binding set and
|
||||
// hence the total binding count.
|
||||
Uint32 activeAttribMask = 0;
|
||||
Uint32 bindingCount = 0;
|
||||
// VkBufferManager::GetSliceEpochCounter() at resolve time. Still equal means
|
||||
// no buffer anywhere changed its slice or was persistently mapped since, which
|
||||
// settles every per-binding question below in one compare.
|
||||
Uint64 sliceEpochCounter = 0;
|
||||
// Any bound buffer already carrying a host map when the slice was resolved.
|
||||
// Such a buffer can mutate its shadow with no API call, so it has to be
|
||||
// re-pushed per draw and the one-compare path above cannot apply.
|
||||
Bool anyBufferMapped = true;
|
||||
|
||||
// Resident element-buffer slice memo (skips the per-draw AcquireResidentSlice
|
||||
// for the VAO's EBO, which cold-chases 500+ distinct resources in a
|
||||
// chunk-cycling frame). Self-validating exactly like the bindings above: a hit
|
||||
// requires the LIVE bound EBO pointer to equal indexBuffer AND either an
|
||||
// unmoved manager-wide slice-epoch counter (nothing anywhere changed slices
|
||||
// or gained a host map, the same one-compare rescue the vertex half uses) or
|
||||
// that buffer's resource still carrying indexSliceEpoch (epochs are minted
|
||||
// from a process-lifetime counter, so a recycled address can never
|
||||
// revalidate). Restart-substituted and streamed EBOs are never stored.
|
||||
// indexFrameSerial tracks the last frame the resource's GPU-use serial was
|
||||
// stamped through this memo; 0 means no index memo. Independent of the
|
||||
// vertex half: both are (pointer, epoch)-validated, so neither can serve
|
||||
// stale state for the other.
|
||||
const MG_State::GLState::BufferObject* indexBuffer = nullptr;
|
||||
Uint64 indexSliceEpoch = 0;
|
||||
// GetSliceEpochCounter() when the resource's epoch was last verified; only
|
||||
// meaningful while indexFrameSerial matches the current frame serial.
|
||||
Uint64 indexSliceEpochCounter = 0;
|
||||
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize indexSliceOffset = 0;
|
||||
Uint64 indexFrameSerial = 0;
|
||||
// The EBO carried a host map when the slice was recorded - the mirror of
|
||||
// anyBufferMapped on the vertex half. A shadow-backed (non-adopted)
|
||||
// persistent map mutates its shadow with no API call and no epoch bump, so
|
||||
// the one-compare rescue must decline and re-run the acquire, whose
|
||||
// SyncPersistentMappedRange is the push-down. A map taken AFTER the record
|
||||
// is already covered: AcquirePersistentMap bumps the slice epoch for the
|
||||
// request itself, adopted or declined.
|
||||
Bool indexBufferMapped = false;
|
||||
|
||||
// Bound per draw (first bindingCount elements).
|
||||
VkBuffer vkBuffers[kMaxBindings] = {};
|
||||
VkDeviceSize vkOffsets[kMaxBindings] = {};
|
||||
// Per binding: the VAO attribute location its buffer comes from, that buffer,
|
||||
// and the buffer's VkBufferManager slice epoch when the slice was resolved.
|
||||
// Only read by the per-frame revalidation and the something-moved fallback.
|
||||
Uint8 attributeLocations[kMaxBindings] = {};
|
||||
const MG_State::GLState::BufferObject* buffers[kMaxBindings] = {};
|
||||
Uint64 sliceEpochs[kMaxBindings] = {};
|
||||
};
|
||||
// One direct-mapped slot of the per-VAO draw-memo table below. A slot belongs to
|
||||
// the object whose (vaoKey, vaoLifetimeId) pair it carries: the address alone
|
||||
// only picks the slot, and the never-reused lifetime id is what proves the slot
|
||||
// is THIS VAO's, so the successor allocated onto a destroyed VAO's address
|
||||
// always misses. That identity check is load-bearing and the content-hash
|
||||
// validations below do NOT stand in for it - a recycled address under a
|
||||
// byte-identical configuration reproduces the content hash exactly, which is
|
||||
// how a destroyed VAO's resolved bindings were once handed to its successor's
|
||||
// draw. The slot is still never dereferenced through vaoKey, and every fact it
|
||||
// carries is still validated against live state before use:
|
||||
// - layoutHash/layoutAuxMasks are valid only while contentHash equals the LIVE
|
||||
// VAO's own hash memo (which the VAO's config version guards), so a config
|
||||
// change or a buffer rebind misses even for the same object.
|
||||
// - bindings revalidates per draw exactly as before (frame serial, content
|
||||
// hash, per-binding live buffer pointers and slice epochs).
|
||||
struct alignas(64) VaoDrawMemo {
|
||||
// P2 D12.4: the handle arm's key, and the ONLY key it needs. {slot, gen} is an
|
||||
// identity, so the pointer-plus-lifetime-id pair below stops being a key here;
|
||||
// the slot also picks the table entry, so the address hash and the two-way probe
|
||||
// go with it. Null in an entry that has never been claimed.
|
||||
MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
const MG_State::GLState::VertexArrayObject* vaoKey = nullptr;
|
||||
// The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer
|
||||
// ALONE is not an identity: a deleted VAO's heap address is handed straight
|
||||
// back by the next glGenVertexArrays-shaped allocation, and the successor then
|
||||
// matched this slot and inherited the dead object's memos. Both stated
|
||||
// defences failed with it, because both reduce to the content hash and the
|
||||
// content hash's buffer-identity component was itself a recycled heap address.
|
||||
Uint64 vaoLifetimeId = 0;
|
||||
// The VAO content hash (VertexInputStateFactory::GetOrComputeHash) the two
|
||||
// layout facts below were derived from; 0 while nothing valid is stored.
|
||||
Uint64 contentHash = 0;
|
||||
Bool layoutFactsValid = false;
|
||||
// The resolved layout identity + packed (unsupported, location) masks -
|
||||
// the exact values GetBackendAuxMemo used to serve, moved here so the
|
||||
// per-draw probe stays inside this table's one hot line instead of
|
||||
// touching a second cold line of every cycled VAO object.
|
||||
Uint64 layoutHash = 0;
|
||||
Uint64 layoutAuxMasks = 0;
|
||||
ResolvedVertexBindings bindings;
|
||||
};
|
||||
// Fixed-size, allocated on first use, never rehashed or swept: entries are
|
||||
// recycled in place on slot collisions (two-slot probe, older frame serial
|
||||
// evicted), and stale entries self-invalidate through the compares above. A
|
||||
// fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
|
||||
// stable for the duration of a draw, which the EBO memo handoff
|
||||
// (m_currentDrawResolvedEntry) relies on.
|
||||
//
|
||||
// [deviation from D12.4, deliberate and narrow] The brief asks for a grow-on-demand
|
||||
// Vector. This one stays FIXED at exactly the capacity and exactly the 2-way victim
|
||||
// rule it has on the base ref, and only its KEY changes (a {slot, gen} handle instead
|
||||
// of a hashed heap address plus a lifetime id). Two reasons, and the second is the
|
||||
// whole of review v2's MAJOR 1:
|
||||
// * a VaoDrawMemo is ~450 B (ResolvedVertexBindings dominates), so growing this
|
||||
// table with the live VAO set is megabytes on a platform with an LMK, where the
|
||||
// other two memos are 48 B and can afford it;
|
||||
// * this is the ONLY one of the three memos that had a capacity before this package.
|
||||
// Losing an entry here costs a vertex-binding re-resolve, exactly what losing it
|
||||
// cost on the base ref, so at any working-set size this table is no worse than what
|
||||
// it replaces - and strictly better below capacity, where the handle is a bijection
|
||||
// with the slot and the two-way probe never collides at all. The other two memos
|
||||
// (VertexInputStateFactory::m_vaoMemos) had NO capacity, so they keep having none.
|
||||
static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
|
||||
Vector<VaoDrawMemo> m_vaoDrawMemoTable;
|
||||
// The renderer's {slot, gen} mint, shared with its VertexInputStateFactory so both
|
||||
// derive the same handle for the same VAO. Per renderer, never a process-global: a
|
||||
// global would share one table and one reclamation clock across two live contexts and
|
||||
// outlive every one of them (review v2 minor 4).
|
||||
MagmaPipeIdentityTables m_pipeIdentity;
|
||||
// The VAO's {slot, gen}. A one-entry memo hit for every acquisition after a draw's
|
||||
// first, so there is no second memo in front of it here.
|
||||
MG_Pipe::MGPipeHandle ResolveVaoHandle(const MG_State::GLState::VertexArrayObject& vao) {
|
||||
return m_pipeIdentity.HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso,
|
||||
vao.GetLifetimeId());
|
||||
}
|
||||
// "Is this VAO's content hash already memoized?", asked of whichever side owns the
|
||||
// memo (P2 D12.5). Force-inlined and defined in the class body so that the PULL
|
||||
// build's three readers keep compiling to the very same two loads they always did -
|
||||
// G1 admits no resize, and an out-of-line call here would be one.
|
||||
[[gnu::always_inline]] inline Bool VaoContentHashIfKnown(
|
||||
const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const {
|
||||
return m_vertexInputStateFactory->TryGetMemoizedHash(vao, outHash);
|
||||
}
|
||||
// Finds the slot holding `vao`, or recycles the older of its two candidate
|
||||
// slots into an empty memo keyed on `vao`. Never returns null.
|
||||
VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao);
|
||||
// The current draw's memo entry, set by UploadAndBindVertexBuffers and consumed
|
||||
// by the same draw's UploadAndBindIndexBuffer (the EBO memo lives in the same
|
||||
// entry). Valid ONLY within that window: the next draw's lookup can recycle the
|
||||
// slot. Null when the draw's layout is not memoisable.
|
||||
ResolvedVertexBindings* m_currentDrawResolvedEntry = nullptr;
|
||||
|
||||
void CreateInstance();
|
||||
VkResult SetupDebugMessenger();
|
||||
@@ -1995,31 +1560,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
|
||||
const IndexBufferView* pIndexBufferView) const;
|
||||
|
||||
VkPipeline GetOrCreatePipeline(
|
||||
GLenum mode,
|
||||
const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
ProgramFactory::CompileOptionFlags transformFlags,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
const RenderPassEntry& renderPassEntry,
|
||||
Bool primitiveRestartEnable);
|
||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||
void DestroyComputePipelines();
|
||||
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
|
||||
// flush the pending recording (see the body), which retires the current command buffer.
|
||||
Bool PrepareStorageImageTextures(
|
||||
FrameContext::FrameData& frame,
|
||||
const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
|
||||
// same image subresource in one shader operation. Snapshot only the sampler side; the
|
||||
// storage descriptor continues to name the application texture.
|
||||
Bool PrepareSamplerImageFeedbackSnapshots(
|
||||
FrameContext::FrameData& frame,
|
||||
const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
VkPipelineStageFlags consumerShaderStageMask);
|
||||
|
||||
// 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.
|
||||
@@ -2040,23 +1582,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
|
||||
}
|
||||
|
||||
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView);
|
||||
// Binds `entry`'s memoised buffers when every input it was resolved from is
|
||||
// still live and unchanged, else returns false and leaves nothing bound.
|
||||
// vaoContentHash is the VAO's memoised content hash (GetBackendHashMemo), which
|
||||
// pins the layout AND the bound buffers without resolving the factory entry.
|
||||
// Non-const entry: a cross-frame revalidation refreshes its serial/epoch stamps.
|
||||
Bool TryBindResolvedVertexBindings(VkCommandBuffer commandBuffer,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
ResolvedVertexBindings& entry,
|
||||
Uint64 vaoContentHash,
|
||||
Uint32 activeAttribMask, Uint64 frameSerial);
|
||||
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
const IndexBufferView* pIndexBufferView = nullptr);
|
||||
Bool InitializeBlitResources();
|
||||
Bool InitializeDepthMipmapResources();
|
||||
void ShutdownBlitResources();
|
||||
@@ -2123,14 +1648,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool MaterializePendingDepthStencilClearForDefaultFramebuffer(
|
||||
VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
const ClearAttachmentPayload& payload);
|
||||
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
VkTextureManager::TextureResource& resource,
|
||||
Uint32 baseMipLevel,
|
||||
Uint32 generateMipLevelCount,
|
||||
const IntVec3& storageBaseTexelSize,
|
||||
VkImageLayout originalLayout,
|
||||
VkImageLayout finalLayout);
|
||||
Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame);
|
||||
|
||||
public:
|
||||
|
||||
@@ -6,34 +6,10 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// Magma's {slot, gen} mint (MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h) and the claim
|
||||
// rule its per-slot memo tables use, at the one point HandleRecycleScenario cannot reach: a
|
||||
// REAL SLOT REUSE.
|
||||
//
|
||||
// WHY THIS SUITE EXISTS (fix-aba review v1, MAJOR 1). The AbaControl integration lanes defeat
|
||||
// the object identity that SELECTS the slot, and that is the whole of what a same-frame pixel
|
||||
// reproducer can defeat:
|
||||
//
|
||||
// * the mint has no death notification, so a slot returns to the free list only through
|
||||
// OnFrameBoundary's age sweep (kSweepInterval 256, kRetireAgeBoundaries 1024);
|
||||
// * HandleRecycleScenario issues five frame boundaries, so its replacement VAO gets a
|
||||
// BRAND-NEW slot at Gen 1 and the generation never participates in a compare;
|
||||
// * a real reuse needs >= 1024 idle boundaries, which puts the two draws in different frames
|
||||
// - and ResolvedVertexBindings, the only memo carrying a GPU slice rather than a layout,
|
||||
// declines across frames by design.
|
||||
//
|
||||
// So deleting the `++m_entries[index].Gen` in MagmaPipeIdentityTable::ClaimSlot leaves every
|
||||
// arm of HandleRecycleScenario green. It reds AnIdleSlotIsRetiredAndReusedWithANewGeneration
|
||||
// and AReusedSlotDoesNotServeItsPredecessorsMemo below, which is the whole point of the file.
|
||||
//
|
||||
// The suite lives beside SlotAllocatorTest because it asserts the same identity contract that
|
||||
// file asserts for the client allocator - "Gen moves on REUSE and never on respecify" - for the
|
||||
// second mint in the tree, the one Magma keeps because nothing in P2 can call the client
|
||||
// allocator's Free (MagmaPipeArms.h says why). It needs no GL context, no driver and no Vulkan
|
||||
// loader: MagmaPipeArms.h is header-only.
|
||||
//
|
||||
// Push-only, like everything it tests, so every case is a visible SKIP in a pull build rather
|
||||
// than a vanishing test (G2 name parity).
|
||||
// Magma's server-side program source and run-ahead capability, the parts of this suite that
|
||||
// outlived the {slot, gen} mint. P13 W6 deleted MagmaPipeIdentityTables and the claim rule
|
||||
// with the frontend arm's VAO memos they keyed (the record arm keys every object on the
|
||||
// applier's own {slot, gen}); their four cases went with them (docs/ci/ci-cleanup-log.md).
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Includes.h"
|
||||
@@ -41,7 +17,6 @@
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
|
||||
#include <Config.h>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Backend/DirectVulkan/Renderer/MagmaProgramSource.h>
|
||||
#endif
|
||||
@@ -52,158 +27,10 @@ namespace {
|
||||
using namespace MobileGL::MG_Backend::DirectVulkan;
|
||||
using MG_Pipe::MGPipeHandle;
|
||||
|
||||
// What VertexInputStateFactory::VaoBackendMemos is, reduced to the two fields the claim
|
||||
// rule needs: the Owner it compares, and one payload word standing in for the memo's
|
||||
// contents (there, the content hash and the resolved-entry pointer). The RULE is production
|
||||
// code - MagmaPipeClaimSlotMemos - not a copy of it.
|
||||
struct TestMemos {
|
||||
MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle;
|
||||
Uint64 Payload = 0;
|
||||
};
|
||||
|
||||
// MagmaPipeIdentityTable's sweep cadence and retirement age are private, so the number of
|
||||
// boundaries needed to retire an object last used at boundary 0 is spelled out here: the
|
||||
// sweep runs when (boundary % 256) == 0 and retires entries idle for more than 1024
|
||||
// boundaries, so the first sweep that can retire it is boundary 1280. Every case that
|
||||
// depends on this ASSERTs the retire actually happened, so a change to either constant
|
||||
// fails loudly instead of silently turning these cases into "two unrelated objects".
|
||||
constexpr int kBoundariesToRetireAnObjectIdleSinceTheStart = 1280;
|
||||
|
||||
class MagmaPipeIdentityTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
m_savedKnob = MG_Config::Features.PipeHandleAbaControl;
|
||||
MG_Config::Features.PipeHandleAbaControl = false;
|
||||
}
|
||||
void TearDown() override { MG_Config::Features.PipeHandleAbaControl = m_savedKnob; }
|
||||
|
||||
// Ages the mint far enough for the sweep to retire everything that has been idle since
|
||||
// the start, while touching `keepAliveLifetimeId` on every boundary so that IT is never
|
||||
// retired. The keep-alive is what makes the reuse non-degenerate: it holds the first
|
||||
// allocatable slot, so the slot under test is not the one negative control C aliases
|
||||
// everything onto (kMagmaPipeAbaControlSlotIndex).
|
||||
static void AgeUntilTheSweepRetiresTheIdleSlots(MagmaPipeIdentityTable& mint,
|
||||
Uint64 keepAliveLifetimeId) {
|
||||
for (int i = 0; i < kBoundariesToRetireAnObjectIdleSinceTheStart; ++i) {
|
||||
mint.Acquire(keepAliveLifetimeId);
|
||||
mint.OnFrameBoundary();
|
||||
}
|
||||
}
|
||||
|
||||
Bool m_savedKnob = false;
|
||||
};
|
||||
|
||||
// The precondition every case below rests on, asserted on its own so that a failure here
|
||||
// reads as "the mint stopped reusing slots" rather than as a memo bug.
|
||||
TEST_F(MagmaPipeIdentityTest, AnIdleSlotIsRetiredAndReusedWithANewGeneration) {
|
||||
MagmaPipeIdentityTable mint("VertexElementsCso");
|
||||
const MGPipeHandle keepAlive = mint.Acquire(1);
|
||||
const MGPipeHandle first = mint.Acquire(2);
|
||||
ASSERT_EQ(keepAlive.Slot, MG_Pipe::kMGPipeFirstAllocatableSlot);
|
||||
ASSERT_NE(first.Slot, keepAlive.Slot);
|
||||
ASSERT_EQ(first.Gen, 1u) << "a slot's first handout is generation 1";
|
||||
ASSERT_EQ(mint.LiveCount(), 2u);
|
||||
|
||||
AgeUntilTheSweepRetiresTheIdleSlots(mint, 1);
|
||||
ASSERT_EQ(mint.LiveCount(), 1u)
|
||||
<< "the idle slot was not retired, so nothing in this file is a slot REUSE";
|
||||
|
||||
// The step HandleRecycleScenario cannot take. With an empty free list this would be a
|
||||
// brand-new slot at Gen 1 and the generation would never participate in any compare -
|
||||
// which is exactly what the scenario measures (redVao slot=2 gen=1, greenVao slot=3
|
||||
// gen=1) and why it cannot catch a deleted ++Gen.
|
||||
const MGPipeHandle second = mint.Acquire(3);
|
||||
EXPECT_EQ(second.Slot, first.Slot) << "a retired slot must come back before the high-water mark";
|
||||
EXPECT_EQ(second.Gen, first.Gen + 1u) << "a slot that changes owner must change generation";
|
||||
EXPECT_FALSE(second == first);
|
||||
EXPECT_EQ(mint.Count(), 2u) << "the reuse must not mint a third slot";
|
||||
}
|
||||
|
||||
// THE CASE THE ++Gen IS LOAD-BEARING FOR. Knob off: the replacement gets the predecessor's
|
||||
// SLOT, so the slot cannot be what separates them - only the generation can.
|
||||
TEST_F(MagmaPipeIdentityTest, AReusedSlotDoesNotServeItsPredecessorsMemo) {
|
||||
MagmaPipeIdentityTable mint("VertexElementsCso");
|
||||
MagmaPipeSlotTable<TestMemos> memos;
|
||||
|
||||
mint.Acquire(1); // the keep-alive, so the slot under test is not slot index 0
|
||||
const MGPipeHandle first = mint.Acquire(2);
|
||||
MagmaPipeClaimSlotMemos(memos, first).Payload = 0xDEADull;
|
||||
ASSERT_TRUE(MagmaPipeClaimSlotMemos(memos, first).Owner == first);
|
||||
ASSERT_EQ(MagmaPipeClaimSlotMemos(memos, first).Payload, 0xDEADull);
|
||||
|
||||
AgeUntilTheSweepRetiresTheIdleSlots(mint, 1);
|
||||
const MGPipeHandle second = mint.Acquire(3);
|
||||
ASSERT_EQ(second.Slot, first.Slot) << "not a slot reuse, so this case would prove nothing";
|
||||
// EXPECT, not ASSERT: with the generation frozen the memo assertion below is exactly what
|
||||
// goes red, and a reader of the failure should see both halves rather than stop here.
|
||||
EXPECT_NE(second.Gen, first.Gen);
|
||||
|
||||
const TestMemos& served = MagmaPipeClaimSlotMemos(memos, second);
|
||||
EXPECT_TRUE(served.Owner == second) << "the entry was not claimed for its new owner";
|
||||
EXPECT_EQ(served.Payload, 0ull)
|
||||
<< "the replacement inherited the dead object's memo out of the SAME slot: the "
|
||||
"generation is the only thing that separates {slot, gen=N} from {slot, gen=N+1}, "
|
||||
"and it did not";
|
||||
}
|
||||
|
||||
// The same shape with negative control C on, which is what makes the case above a control
|
||||
// rather than a tautology: with the knob on the memo IS served across the generation.
|
||||
//
|
||||
// The knob is set before the first claim, as a process-wide knob is in a real run: what it
|
||||
// defeats is the identity that selects the entry, so a run that stamps with it off and reads
|
||||
// with it on would be reading a different entry, not an aliased one.
|
||||
TEST_F(MagmaPipeIdentityTest, TheAbaControlKnobServesTheStaleMemoAcrossAReusedSlot) {
|
||||
MG_Config::Features.PipeHandleAbaControl = true;
|
||||
|
||||
MagmaPipeIdentityTable mint("VertexElementsCso");
|
||||
MagmaPipeSlotTable<TestMemos> memos;
|
||||
|
||||
mint.Acquire(1);
|
||||
const MGPipeHandle first = mint.Acquire(2);
|
||||
TestMemos& stamped = MagmaPipeClaimSlotMemos(memos, first);
|
||||
stamped.Payload = 0xDEADull;
|
||||
|
||||
AgeUntilTheSweepRetiresTheIdleSlots(mint, 1);
|
||||
const MGPipeHandle second = mint.Acquire(3);
|
||||
ASSERT_EQ(second.Slot, first.Slot);
|
||||
EXPECT_NE(second.Gen, first.Gen);
|
||||
|
||||
const TestMemos& served = MagmaPipeClaimSlotMemos(memos, second);
|
||||
EXPECT_EQ(&served, &stamped) << "the control must collapse every object onto one entry";
|
||||
EXPECT_EQ(served.Payload, 0xDEADull)
|
||||
<< "negative control C is not defeating the claim rule any more: the replacement was "
|
||||
"NOT handed its predecessor's memo, so the AbaControl lanes assert nothing";
|
||||
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(served.Owner))
|
||||
<< "the control hands the entry back UNCLEARED and UNCLAIMED - it never learns whose "
|
||||
"it is, which is what 'replace the identity with a constant' means";
|
||||
}
|
||||
|
||||
// The other half of the {slot, gen} contract, and the reason a deleted ++Gen cannot be
|
||||
// 'fixed' by bumping Gen on every acquisition: a live object keeps its handle across
|
||||
// sweeps, so its memo survives a reconfiguration instead of being recomputed per draw.
|
||||
TEST_F(MagmaPipeIdentityTest, ALiveObjectKeepsItsSlotItsGenerationAndItsMemo) {
|
||||
MagmaPipeIdentityTable mint("VertexElementsCso");
|
||||
MagmaPipeSlotTable<TestMemos> memos;
|
||||
|
||||
const MGPipeHandle handle = mint.Acquire(2);
|
||||
MagmaPipeClaimSlotMemos(memos, handle).Payload = 0xBEEFull;
|
||||
|
||||
// Past two sweeps (256 and 512), drawn on every boundary.
|
||||
for (int i = 0; i < 700; ++i) {
|
||||
mint.Acquire(2);
|
||||
mint.OnFrameBoundary();
|
||||
}
|
||||
const MGPipeHandle again = mint.Acquire(2);
|
||||
EXPECT_TRUE(again == handle) << "a live object's handle moved under the age sweep";
|
||||
EXPECT_EQ(MagmaPipeClaimSlotMemos(memos, again).Payload, 0xBEEFull)
|
||||
<< "a live object's memo was cleared without its slot changing owner";
|
||||
EXPECT_EQ(mint.Count(), 1u);
|
||||
}
|
||||
|
||||
// Historical test name retained. Each backend's readiness is now a real gate:
|
||||
// false forbids the capability, true permits it. Production keeps its own
|
||||
// Magma readiness constant, which the bootstrap and GPU queue tests also check.
|
||||
TEST_F(MagmaPipeIdentityTest, AMagmaServerNeverPublishesTheRunAheadCapBit) {
|
||||
TEST(MagmaPipeIdentityTest, AMagmaServerNeverPublishesTheRunAheadCapBit) {
|
||||
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/false), 0u);
|
||||
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/true),
|
||||
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
|
||||
@@ -244,8 +71,6 @@ namespace {
|
||||
record.GlobalConstants = {5, 6};
|
||||
record.GlobalConstantsVersion = 11;
|
||||
const MagmaProgramSource source({7, 2}, record);
|
||||
EXPECT_TRUE(source.IsWire());
|
||||
EXPECT_EQ(source.Frontend(), nullptr);
|
||||
EXPECT_EQ(source.GetUniformBlockBinding(0), 3u);
|
||||
EXPECT_EQ(source.GetUniformBlockName(0), "Block");
|
||||
EXPECT_EQ(source.GetUBOSizeAt(0), 16u);
|
||||
|
||||
Reference in New Issue
Block a user