mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Refactor] (P13 W6b): the data-arm predicate folds out of every backend branch; frontend-arm unit cases go, three adapted, and two GL sampling rules move to an integration scenario.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2201,8 +2201,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// before this). So under split the subdata MINTS the twin it needs to stage into.
|
||||
// Monolith is untouched: the twin stays lazy there because the frontend object's
|
||||
// MappedData is the source and nothing is lost by deferring the allocation (D-A2).
|
||||
if (resource == nullptr && bytes != nullptr &&
|
||||
MG_Config::DataArmIsRecord()) {
|
||||
if (resource == nullptr && bytes != nullptr) {
|
||||
resource = GetOrCreateBufferResourceForHandle(res);
|
||||
}
|
||||
if (!resource) return;
|
||||
@@ -2406,8 +2405,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// An empty capture may conservatively request a writeback before
|
||||
// any draw materialized this buffer. Resolve its staged resource
|
||||
// exactly as a GPU consumer would, then return the real bytes.
|
||||
auto* resource = MG_Config::DataArmIsRecord()
|
||||
? EnsureBufferResourceForHandle(nullptr, res) : FindBufferResourceForHandle(res);
|
||||
auto* resource = EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
|
||||
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
|
||||
if (resource->persistentMapped) {
|
||||
@@ -3893,9 +3891,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
//
|
||||
// A null object is the "no frontend to ask" case and is treated as "not mapped",
|
||||
// which is what the record already says.
|
||||
const Bool askTheObjectWhetherItIsMapped =
|
||||
!MG_Config::DataArmIsRecord();
|
||||
if (askTheObjectWhetherItIsMapped && frontend != nullptr && frontend->IsMapped()) return false;
|
||||
if (resource->pendingRespecify || !resource->storageInitialized) return false;
|
||||
if (!resource->pendingRanges.empty()) return false;
|
||||
if (resource->storageSize != static_cast<SizeT>(record->Desc.Width)) return false;
|
||||
@@ -3960,9 +3955,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// so a corrupt staged upload rendered the frontend's correct bytes and the R-2.5
|
||||
// 0xDD audit could not reach a draw. R-2 / table 3: honest inproc is inproc that
|
||||
// does not read the client object's memory. Under monolith nothing changes.
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
return resource->hostBytes;
|
||||
}
|
||||
return resource->hostBytes;
|
||||
if (bufferObject) return bufferObject->MappedData();
|
||||
return resource->hostBytes;
|
||||
};
|
||||
@@ -4099,9 +4092,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// layer-1 surface ("buffer-legacy-arm") and the client's own pre-verb
|
||||
// persistent-map push is the only producer; the call is skipped outright. Under
|
||||
// monolith D-N's placement stands.
|
||||
if (bufferObject && !MG_Config::DataArmIsRecord()) {
|
||||
bufferObject->SyncPersistentMappedRange();
|
||||
}
|
||||
|
||||
const auto* record = ResourceRecordOf(res);
|
||||
const SizeT size = record != nullptr ? static_cast<SizeT>(record->Desc.Width) : 0;
|
||||
@@ -4144,12 +4134,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the last orphan". With no coverage and a clear bit the store is undefined by the
|
||||
// application's own declaration and the upload stays a pure NULL reallocation.
|
||||
const Bool shadowHasContent =
|
||||
MG_Config::DataArmIsRecord()
|
||||
? (record->Desc.HasDefinedContent != 0 ||
|
||||
(record->Desc.HasDefinedContent != 0 ||
|
||||
(ServerStaged().CopiesIntoServerStorage() ? ServerStaged().CoveredRunCount(resource) != 0
|
||||
: resource->contentSinceRespecify))
|
||||
:
|
||||
(bufferObject ? bufferObject->HasDefinedContent() : (record->Desc.HasDefinedContent != 0));
|
||||
: resource->contentSinceRespecify));
|
||||
const void* initialData = shadowHasContent ? hostBase : nullptr;
|
||||
// M-3 / codex 4: a RespecifyStorageWith(..., initialData != nullptr) is a WHOLE-STORE
|
||||
// [0, size) upload from the base, so it owes the same coverage the pending-range drain
|
||||
@@ -5018,8 +5005,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemFramebuffer (bit 9) clear (or refuses "
|
||||
"it) and MOBILEGL_PIPE_LEGACY_MEMOS=0 disables the pre-handle g_fboSynced* arm");
|
||||
}
|
||||
if (MG_Config::DataArmIsRecord() &&
|
||||
verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
|
||||
if (verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
|
||||
StopOnTransportWithoutRecordArm(
|
||||
"kMGPipeSubsystemFramebuffer (bit 9)",
|
||||
"the pre-handle g_fboSynced* / g_fboTextureSyncList arm reads the framebuffer "
|
||||
@@ -5061,8 +5047,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"refuses it) and MOBILEGL_PIPE_LEGACY_MEMOS=0 disables the pre-handle texture "
|
||||
"cheap-gate trio");
|
||||
}
|
||||
if (MG_Config::DataArmIsRecord() &&
|
||||
verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
|
||||
if (verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
|
||||
StopOnTransportWithoutRecordArm(
|
||||
"kMGPipeSubsystemTextureResources (bit 10)",
|
||||
"the pre-handle texture cheap-gate trio resolves through the frontend texture "
|
||||
@@ -5095,8 +5080,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"it) and MOBILEGL_PIPE_LEGACY_MEMOS=0 disables UnitSamplerLookupMemo's WeakPtr "
|
||||
"arm and SamplerPassMemo's raw-pointer rows");
|
||||
}
|
||||
if (MG_Config::DataArmIsRecord() &&
|
||||
verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
|
||||
if (verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
|
||||
StopOnTransportWithoutRecordArm(
|
||||
"kMGPipeSubsystemSamplers (bit 11)",
|
||||
"UnitSamplerLookupMemo's WeakPtr arm and SamplerPassMemo's raw-pointer rows are "
|
||||
@@ -5122,8 +5106,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemPrograms (bit 12) clear and "
|
||||
"MOBILEGL_PIPE_LEGACY_MEMOS=0 disables g_programTwinLookupMemo");
|
||||
}
|
||||
if (MG_Config::DataArmIsRecord() &&
|
||||
verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
|
||||
if (verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
|
||||
StopOnTransportWithoutRecordArm(
|
||||
"kMGPipeSubsystemPrograms (bit 12)",
|
||||
"g_programTwinLookupMemo is keyed on the frontend ProgramObject and the legacy arm "
|
||||
@@ -6279,7 +6262,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// at adoption for the same reason, so the fallback below is a null and a refusal
|
||||
// rather than a stale read.
|
||||
const Bool sourceGpuWritten =
|
||||
MG_Config::DataArmIsRecord() && resource->serverGpuWritten;
|
||||
resource->serverGpuWritten;
|
||||
if (sourceGpuWritten) {
|
||||
BufferImpl::SplitHostBytesForCpuRead(*resource, binding.Res, "fp64_vertex_narrowing");
|
||||
}
|
||||
@@ -6463,153 +6446,151 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
// P13 W4b (defect h): monolith's record arm takes the body too; its old arm only
|
||||
// cleared m_isInitialized and replayed the frontend shadow at the next sync.
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const auto format = static_cast<TextureInternalFormat>(record.Desc.InternalFormat);
|
||||
// ES image binding needs immutable storage, but an already
|
||||
// immutable core-format allocation needs no replacement. Keep
|
||||
// its GPU contents and native name; normal serial/shape checks
|
||||
// below still consume pending uploads and real redefinitions.
|
||||
if (m_isInitialized && m_backendStorageImmutable && !GetImageBindableStorageWidening(format)) {
|
||||
m_imageBindableStorageRequired = true;
|
||||
m_forceTextureParamsResync = true;
|
||||
return;
|
||||
}
|
||||
// P9 W2 (notes/p9/W2-REMINT.md): EVERY BYTE THIS RE-MINT REPLAYS IS ALREADY ON THE
|
||||
// SERVER. The GPU image holds what the GPU wrote (render targets, clears, copies,
|
||||
// generated mips, image stores) and the staged store holds what the client uploaded;
|
||||
// the client is never asked, so no pull protocol exists. Reading only the store would
|
||||
// be wrong: a GPU-written level's store run is stale or absent - both host corpus
|
||||
// re-mints (iris-photon, iris-derivative) are render targets with no staged bytes.
|
||||
const Bool hadBackendStorage = m_isInitialized;
|
||||
// The R-16 control for W2-b's fallback below: a readback the driver refuses cannot be
|
||||
// produced on the host (llvmpipe renders every image format), so the test lane forces
|
||||
// one. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs.
|
||||
static const Bool s_forceReadbackFailure =
|
||||
std::getenv("MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE") != nullptr;
|
||||
auto& store = MG_Record::ServerStagedTexture();
|
||||
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res);
|
||||
const auto target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
|
||||
GLenum canonicalFormat = GL_NONE, canonicalType = GL_NONE;
|
||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
|
||||
MG_Util::ConvertTextureInternalFormatToGLEnum(format), {}, nullptr,
|
||||
&canonicalFormat, &canonicalType);
|
||||
for (const auto& uploadTarget :
|
||||
BufferImpl::StagedUploadTargetsForPipeTarget(record.Desc.Target)) {
|
||||
for (Uint32 level = 0; level < record.Desc.Levels; ++level) {
|
||||
const auto targetCode = static_cast<Uint16>(uploadTarget);
|
||||
const auto levelCode = static_cast<Uint16>(level);
|
||||
const IntVec3 extent = store.LevelExtentOrUndefined(key, targetCode, levelCode);
|
||||
if (extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) continue;
|
||||
const auto* pending = FindPipeTextureUpload(record, targetCode, levelCode);
|
||||
const Bool covered = store.IsCovered(key, targetCode, levelCode);
|
||||
const MG_Pipe::MGPBox whole{0, 0, 0, static_cast<Uint32>(extent.x()),
|
||||
static_cast<Uint32>(extent.y()), static_cast<Uint32>(extent.z())};
|
||||
// A genuinely full pending write owns every byte. A
|
||||
// union box covering the level is not sufficient when
|
||||
// its explicit rectangles leave GPU-written gaps.
|
||||
const auto coversWhole = [&](const auto& box) {
|
||||
return box.X == 0 && box.Y == 0 && box.Z == 0 &&
|
||||
box.W == whole.W && box.H == whole.H && box.D == whole.D;
|
||||
};
|
||||
const Bool pendingWhole = pending && (pending->Regions.empty()
|
||||
? coversWhole(pending->UnionBox)
|
||||
: std::any_of(pending->Regions.begin(), pending->Regions.end(), coversWhole));
|
||||
// W2-a: ONLY A LEVEL THE DRIVER ACTUALLY HOLDS IS READ BACK. The store's
|
||||
// Defined-ness follows the client's respecifies, the driver's follows this
|
||||
// twin's last sync, and the two differ for a level defined (null data, or null
|
||||
// data plus a partial upload) after that sync: the driver has no such level, so
|
||||
// the GPU cannot have written it. A covered level then replays its staged run
|
||||
// (the client's whole-level shadow, pending boxes included) and an uncovered one
|
||||
// is undefined content with nothing to preserve. Reading it anyway was
|
||||
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
|
||||
const Bool nativeLevel =
|
||||
m_isInitialized &&
|
||||
NativeTextureLevelHasExtent(m_backendTextureId, target, uploadTarget,
|
||||
static_cast<GLint>(level), extent);
|
||||
if (nativeLevel && !(covered && pendingWhole)) {
|
||||
Vector<Uint8> snapshot;
|
||||
const Bool readBack =
|
||||
!s_forceReadbackFailure &&
|
||||
ReadTextureLevelTight(m_backendTextureId, target, uploadTarget, format,
|
||||
static_cast<GLint>(level), extent,
|
||||
/*sourceUsesImageCarrier=*/false, canonicalFormat,
|
||||
canonicalType, snapshot);
|
||||
if (!readBack && !covered) {
|
||||
// The GPU holds this level, the driver will not hand it back, and no
|
||||
// CPU byte of it exists anywhere: the one case with no honest answer.
|
||||
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-readback\"} {%u,%u} level=%u",
|
||||
res.Slot, res.Gen, level);
|
||||
std::abort();
|
||||
}
|
||||
if (!readBack) {
|
||||
// W2-b: the driver refused the readback (a format it cannot attach
|
||||
// to a framebuffer - SNORM without EXT_render_snorm, 16-bit norm
|
||||
// without EXT_texture_norm16, float without EXT_color_buffer_float)
|
||||
// but the store covers the level. Its staged run is exactly what the
|
||||
// monolith re-mint replays; texels the GPU wrote since that upload
|
||||
// are not preserved, which is monolith's answer too.
|
||||
MGLOG_W_ONCE("MGPipe: image promotion of texture {%u,%u} level %u: the driver "
|
||||
"refused the readback (internal format %u); replaying the level's "
|
||||
"staged bytes instead (remint-readback-fallback)",
|
||||
res.Slot, res.Gen, level, record.Desc.InternalFormat);
|
||||
}
|
||||
if (readBack && pending) {
|
||||
// Native contents include prior draws/image
|
||||
// writes; the pending stage owns ONLY its boxes.
|
||||
// Merge those boxes before adopting a whole-level
|
||||
// snapshot, never overwrite untouched GPU texels
|
||||
// with the rest of a stale client staging run.
|
||||
const SizeT pixels = static_cast<SizeT>(extent.x()) * extent.y() * extent.z();
|
||||
const SizeT stagedSize = store.LevelByteSize(key, targetCode, levelCode);
|
||||
if (!covered || !pixels || snapshot.size() != stagedSize || stagedSize % pixels) {
|
||||
MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"image-promotion-pending\"}");
|
||||
std::abort();
|
||||
}
|
||||
const SizeT bpp = stagedSize / pixels;
|
||||
const Uint8* staged = store.RequireLevelBytes(key, targetCode, levelCode, "image-promotion");
|
||||
const auto merge = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) {
|
||||
if (x < 0 || y < 0 || z < 0 || Uint64(x) + w > whole.W ||
|
||||
Uint64(y) + h > whole.H || Uint64(z) + d > whole.D) {
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"image-promotion-region\"}");
|
||||
std::abort();
|
||||
}
|
||||
for (Uint32 slice = 0; slice < d; ++slice) {
|
||||
for (Uint32 row = 0; row < h; ++row) {
|
||||
const SizeT offset = ((static_cast<SizeT>(z + slice) * whole.H + y + row) * whole.W + x) * bpp;
|
||||
Memcpy(snapshot.data() + offset, staged + offset, static_cast<SizeT>(w) * bpp);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (pending->Regions.empty()) {
|
||||
const auto& box = pending->UnionBox;
|
||||
merge(box.X, box.Y, box.Z, box.W, box.H, box.D);
|
||||
} else {
|
||||
for (const auto& box : pending->Regions)
|
||||
merge(box.X, box.Y, box.Z, box.W, box.H, box.D);
|
||||
}
|
||||
}
|
||||
if (readBack) {
|
||||
store.Adopt(key, targetCode, levelCode, extent, snapshot.data(), snapshot.size());
|
||||
}
|
||||
}
|
||||
// Undefined contents have no byte run to preserve. A
|
||||
// first allocation can still be image-bindable without
|
||||
// inventing an upload or consulting the frontend.
|
||||
if (!store.IsCovered(key, targetCode, levelCode)) continue;
|
||||
if (!RearmPipeTextureLevelUpload(res,
|
||||
MG_Pipe::MGPipePackSubDataTarget(record.Desc.Target, static_cast<Uint32>(uploadTarget)),
|
||||
levelCode, whole)) {
|
||||
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-pending-capacity\"}");
|
||||
const auto format = static_cast<TextureInternalFormat>(record.Desc.InternalFormat);
|
||||
// ES image binding needs immutable storage, but an already
|
||||
// immutable core-format allocation needs no replacement. Keep
|
||||
// its GPU contents and native name; normal serial/shape checks
|
||||
// below still consume pending uploads and real redefinitions.
|
||||
if (m_isInitialized && m_backendStorageImmutable && !GetImageBindableStorageWidening(format)) {
|
||||
m_imageBindableStorageRequired = true;
|
||||
m_forceTextureParamsResync = true;
|
||||
return;
|
||||
}
|
||||
// P9 W2 (notes/p9/W2-REMINT.md): EVERY BYTE THIS RE-MINT REPLAYS IS ALREADY ON THE
|
||||
// SERVER. The GPU image holds what the GPU wrote (render targets, clears, copies,
|
||||
// generated mips, image stores) and the staged store holds what the client uploaded;
|
||||
// the client is never asked, so no pull protocol exists. Reading only the store would
|
||||
// be wrong: a GPU-written level's store run is stale or absent - both host corpus
|
||||
// re-mints (iris-photon, iris-derivative) are render targets with no staged bytes.
|
||||
const Bool hadBackendStorage = m_isInitialized;
|
||||
// The R-16 control for W2-b's fallback below: a readback the driver refuses cannot be
|
||||
// produced on the host (llvmpipe renders every image format), so the test lane forces
|
||||
// one. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs.
|
||||
static const Bool s_forceReadbackFailure =
|
||||
std::getenv("MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE") != nullptr;
|
||||
auto& store = MG_Record::ServerStagedTexture();
|
||||
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res);
|
||||
const auto target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
|
||||
GLenum canonicalFormat = GL_NONE, canonicalType = GL_NONE;
|
||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
|
||||
MG_Util::ConvertTextureInternalFormatToGLEnum(format), {}, nullptr,
|
||||
&canonicalFormat, &canonicalType);
|
||||
for (const auto& uploadTarget :
|
||||
BufferImpl::StagedUploadTargetsForPipeTarget(record.Desc.Target)) {
|
||||
for (Uint32 level = 0; level < record.Desc.Levels; ++level) {
|
||||
const auto targetCode = static_cast<Uint16>(uploadTarget);
|
||||
const auto levelCode = static_cast<Uint16>(level);
|
||||
const IntVec3 extent = store.LevelExtentOrUndefined(key, targetCode, levelCode);
|
||||
if (extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) continue;
|
||||
const auto* pending = FindPipeTextureUpload(record, targetCode, levelCode);
|
||||
const Bool covered = store.IsCovered(key, targetCode, levelCode);
|
||||
const MG_Pipe::MGPBox whole{0, 0, 0, static_cast<Uint32>(extent.x()),
|
||||
static_cast<Uint32>(extent.y()), static_cast<Uint32>(extent.z())};
|
||||
// A genuinely full pending write owns every byte. A
|
||||
// union box covering the level is not sufficient when
|
||||
// its explicit rectangles leave GPU-written gaps.
|
||||
const auto coversWhole = [&](const auto& box) {
|
||||
return box.X == 0 && box.Y == 0 && box.Z == 0 &&
|
||||
box.W == whole.W && box.H == whole.H && box.D == whole.D;
|
||||
};
|
||||
const Bool pendingWhole = pending && (pending->Regions.empty()
|
||||
? coversWhole(pending->UnionBox)
|
||||
: std::any_of(pending->Regions.begin(), pending->Regions.end(), coversWhole));
|
||||
// W2-a: ONLY A LEVEL THE DRIVER ACTUALLY HOLDS IS READ BACK. The store's
|
||||
// Defined-ness follows the client's respecifies, the driver's follows this
|
||||
// twin's last sync, and the two differ for a level defined (null data, or null
|
||||
// data plus a partial upload) after that sync: the driver has no such level, so
|
||||
// the GPU cannot have written it. A covered level then replays its staged run
|
||||
// (the client's whole-level shadow, pending boxes included) and an uncovered one
|
||||
// is undefined content with nothing to preserve. Reading it anyway was
|
||||
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
|
||||
const Bool nativeLevel =
|
||||
m_isInitialized &&
|
||||
NativeTextureLevelHasExtent(m_backendTextureId, target, uploadTarget,
|
||||
static_cast<GLint>(level), extent);
|
||||
if (nativeLevel && !(covered && pendingWhole)) {
|
||||
Vector<Uint8> snapshot;
|
||||
const Bool readBack =
|
||||
!s_forceReadbackFailure &&
|
||||
ReadTextureLevelTight(m_backendTextureId, target, uploadTarget, format,
|
||||
static_cast<GLint>(level), extent,
|
||||
/*sourceUsesImageCarrier=*/false, canonicalFormat,
|
||||
canonicalType, snapshot);
|
||||
if (!readBack && !covered) {
|
||||
// The GPU holds this level, the driver will not hand it back, and no
|
||||
// CPU byte of it exists anywhere: the one case with no honest answer.
|
||||
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-readback\"} {%u,%u} level=%u",
|
||||
res.Slot, res.Gen, level);
|
||||
std::abort();
|
||||
}
|
||||
if (!readBack) {
|
||||
// W2-b: the driver refused the readback (a format it cannot attach
|
||||
// to a framebuffer - SNORM without EXT_render_snorm, 16-bit norm
|
||||
// without EXT_texture_norm16, float without EXT_color_buffer_float)
|
||||
// but the store covers the level. Its staged run is exactly what the
|
||||
// monolith re-mint replays; texels the GPU wrote since that upload
|
||||
// are not preserved, which is monolith's answer too.
|
||||
MGLOG_W_ONCE("MGPipe: image promotion of texture {%u,%u} level %u: the driver "
|
||||
"refused the readback (internal format %u); replaying the level's "
|
||||
"staged bytes instead (remint-readback-fallback)",
|
||||
res.Slot, res.Gen, level, record.Desc.InternalFormat);
|
||||
}
|
||||
if (readBack && pending) {
|
||||
// Native contents include prior draws/image
|
||||
// writes; the pending stage owns ONLY its boxes.
|
||||
// Merge those boxes before adopting a whole-level
|
||||
// snapshot, never overwrite untouched GPU texels
|
||||
// with the rest of a stale client staging run.
|
||||
const SizeT pixels = static_cast<SizeT>(extent.x()) * extent.y() * extent.z();
|
||||
const SizeT stagedSize = store.LevelByteSize(key, targetCode, levelCode);
|
||||
if (!covered || !pixels || snapshot.size() != stagedSize || stagedSize % pixels) {
|
||||
MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"image-promotion-pending\"}");
|
||||
std::abort();
|
||||
}
|
||||
const SizeT bpp = stagedSize / pixels;
|
||||
const Uint8* staged = store.RequireLevelBytes(key, targetCode, levelCode, "image-promotion");
|
||||
const auto merge = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) {
|
||||
if (x < 0 || y < 0 || z < 0 || Uint64(x) + w > whole.W ||
|
||||
Uint64(y) + h > whole.H || Uint64(z) + d > whole.D) {
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"image-promotion-region\"}");
|
||||
std::abort();
|
||||
}
|
||||
for (Uint32 slice = 0; slice < d; ++slice) {
|
||||
for (Uint32 row = 0; row < h; ++row) {
|
||||
const SizeT offset = ((static_cast<SizeT>(z + slice) * whole.H + y + row) * whole.W + x) * bpp;
|
||||
Memcpy(snapshot.data() + offset, staged + offset, static_cast<SizeT>(w) * bpp);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (pending->Regions.empty()) {
|
||||
const auto& box = pending->UnionBox;
|
||||
merge(box.X, box.Y, box.Z, box.W, box.H, box.D);
|
||||
} else {
|
||||
for (const auto& box : pending->Regions)
|
||||
merge(box.X, box.Y, box.Z, box.W, box.H, box.D);
|
||||
}
|
||||
}
|
||||
if (readBack) {
|
||||
store.Adopt(key, targetCode, levelCode, extent, snapshot.data(), snapshot.size());
|
||||
}
|
||||
}
|
||||
// Undefined contents have no byte run to preserve. A
|
||||
// first allocation can still be image-bindable without
|
||||
// inventing an upload or consulting the frontend.
|
||||
if (!store.IsCovered(key, targetCode, levelCode)) continue;
|
||||
if (!RearmPipeTextureLevelUpload(res,
|
||||
MG_Pipe::MGPipePackSubDataTarget(record.Desc.Target, static_cast<Uint32>(uploadTarget)),
|
||||
levelCode, whole)) {
|
||||
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-pending-capacity\"}");
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
// W2-d: `trp=` counts a re-mint of storage the backend ALREADY held, on this arm as
|
||||
// on the frontend one (which counts in RequireImageBindableStorage). The keep path
|
||||
// above is not a re-mint and returns before this; a first allocation holds nothing.
|
||||
if (hadBackendStorage && MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1);
|
||||
}
|
||||
}
|
||||
// W2-d: `trp=` counts a re-mint of storage the backend ALREADY held, on this arm as
|
||||
// on the frontend one (which counts in RequireImageBindableStorage). The keep path
|
||||
// above is not a re-mint and returns before this; a first allocation holds nothing.
|
||||
if (hadBackendStorage && MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1);
|
||||
}
|
||||
// Every defined byte now belongs to the server and every required
|
||||
// replay is in its pending set. The existing upload path builds and
|
||||
@@ -7759,7 +7740,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// to be transport-gated now say so themselves, so a future by-handle noter cannot quietly
|
||||
// re-open ID-107's hole somewhere else.
|
||||
#define MGB_TEXTURE_RECORD_ARM_SELECTED() \
|
||||
(MG_Config::DataArmIsRecord() && TextureResourceSubsystemEnabled())
|
||||
(TextureResourceSubsystemEnabled())
|
||||
|
||||
// Returns true when the caller must decline (no handle noted: the caller's bug it always
|
||||
// was, named by the caller's own log line), false when the record arm can serve the call,
|
||||
@@ -8373,12 +8354,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Ruling 1's arm: the push-monolith build keeps the frontend test below token for
|
||||
// token, because there the two answers are the same answer and the verify comparator
|
||||
// needs the frontend arm.
|
||||
if (MG_Config::DataArmIsRecord() && pushedStorage != nullptr &&
|
||||
if (pushedStorage != nullptr &&
|
||||
!MG_Pipe::MGPipeHandleIsNull(pushedStorage->Desc.ViewOf)) {
|
||||
SyncTextureViewToBackendByRecord(pushedRes, *pushedStorage, stateTextureObject);
|
||||
return;
|
||||
}
|
||||
if (MG_Config::DataArmIsRecord() && pushedStorage != nullptr) {
|
||||
if (pushedStorage != nullptr) {
|
||||
// Not a view: fall through to the storage body with the frontend test skipped.
|
||||
} else
|
||||
// A texture created by glTextureView owns no storage: the levels, the format and
|
||||
@@ -8431,10 +8412,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// P5e (tx2), ruling 1's arm: under a transport the re-dirty half is a NAMED
|
||||
// refusal at its entry (§5.2) instead of a walk of the client's level shadows;
|
||||
// the push-monolith build keeps the frontend transition exactly as it is.
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
RequireImageBindableStorageByHandle(pushedRes, *pushedStorage);
|
||||
} else
|
||||
RequireImageBindableStorage(stateTextureObject);
|
||||
RequireImageBindableStorageByHandle(pushedRes, *pushedStorage);
|
||||
}
|
||||
|
||||
// ONE compare replaces the whole cheap-gate trio AND the content version. The
|
||||
@@ -11136,30 +11114,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// push-monolith arm keeps the frontend sync it had at f6cfcbd3, token for token,
|
||||
// because tx2's record arm inside SyncMipmapsToBackend is itself selected by
|
||||
// `Transport != Monolith` and cannot answer here.
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
backendTextureObject->SyncMipmapsToBackendByHandle(surface.Res);
|
||||
// From here on every draw, clear and blit into this framebuffer can write the
|
||||
// texture behind the staged shadow's back (the frontend arm's note, by handle).
|
||||
TextureImpl::NoteDriverSideTextureWriteByHandle(surface.Res);
|
||||
} else
|
||||
{
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> monolithTexture =
|
||||
(monolithAttachment != nullptr && monolithAttachment->IsTexture())
|
||||
? monolithAttachment->GetTexture()
|
||||
: nullptr;
|
||||
if (!monolithTexture) {
|
||||
MGLOG_E_ONCE("MGPipe: attachment record names texture {%u, %u} on the "
|
||||
"push-monolith arm, where the storage sync needs the frontend "
|
||||
"texture and this caller supplied none - refusing to attach a "
|
||||
"texture whose levels were never pushed",
|
||||
surface.Res.Slot, surface.Res.Gen);
|
||||
return false;
|
||||
}
|
||||
backendTextureObject->SyncMipmapsToBackend(monolithTexture);
|
||||
// From here on every draw, clear and blit into this framebuffer can write the
|
||||
// texture behind the shadow's back.
|
||||
TextureImpl::NoteDriverSideTextureWrite(monolithTexture);
|
||||
}
|
||||
backendTextureObject->SyncMipmapsToBackendByHandle(surface.Res);
|
||||
// From here on every draw, clear and blit into this framebuffer can write the
|
||||
// texture behind the staged shadow's back (the frontend arm's note, by handle).
|
||||
TextureImpl::NoteDriverSideTextureWriteByHandle(surface.Res);
|
||||
const auto uploadTarget = static_cast<TextureUploadTarget>(surface.UploadTarget);
|
||||
if (surface.Layered != 0) {
|
||||
g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment,
|
||||
@@ -11223,24 +11181,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// GLContext (RecordError) and the by-handle form deliberately does not, because
|
||||
// under a transport there is no application on this side to report it to. On the
|
||||
// push-monolith arm there is, so the monolith arm keeps the object form.
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
backendRenderbufferObject->SyncToBackendByHandle(surface.Res);
|
||||
} else
|
||||
{
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject> monolithRenderbuffer =
|
||||
(monolithAttachment != nullptr && monolithAttachment->IsRenderbuffer())
|
||||
? monolithAttachment->GetRenderbuffer()
|
||||
: nullptr;
|
||||
if (!monolithRenderbuffer) {
|
||||
MGLOG_E_ONCE("MGPipe: attachment record names renderbuffer {%u, %u} on the "
|
||||
"push-monolith arm, where the storage allocation needs the "
|
||||
"frontend renderbuffer and this caller supplied none - refusing "
|
||||
"to attach a renderbuffer with no storage",
|
||||
surface.Res.Slot, surface.Res.Gen);
|
||||
return false;
|
||||
}
|
||||
backendRenderbufferObject->SyncToBackend(monolithRenderbuffer);
|
||||
}
|
||||
backendRenderbufferObject->SyncToBackendByHandle(surface.Res);
|
||||
backendRenderbufferObject->Bind();
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
|
||||
backendRenderbufferObject->GetBackendRenderbufferId());
|
||||
@@ -11273,10 +11214,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// (the three-channel colour-renderable widening). GL answers such a read with 1.0, but
|
||||
// the storage holds whatever the draw wrote there, so the readback has to overwrite it.
|
||||
Bool IsAlphaWidenedFallbackReadAttachment() {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer();
|
||||
return record && IsAlphaWidenedColorSurface(record->ReadSurface);
|
||||
}
|
||||
const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer();
|
||||
return record && IsAlphaWidenedColorSurface(record->ReadSurface);
|
||||
const auto* attachmentObject = GetReadColorAttachment();
|
||||
if (attachmentObject == nullptr) {
|
||||
return false;
|
||||
@@ -11285,11 +11224,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
Bool IsFixedPointFallbackReadAttachment() {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer();
|
||||
return record && record->ReadSurface.Kind != MG_Pipe::kMGPipeSurfaceKindNone &&
|
||||
IsSnormFormat(static_cast<TextureInternalFormat>(record->ReadSurface.InternalFormat));
|
||||
}
|
||||
const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer();
|
||||
return record && record->ReadSurface.Kind != MG_Pipe::kMGPipeSurfaceKindNone &&
|
||||
IsSnormFormat(static_cast<TextureInternalFormat>(record->ReadSurface.InternalFormat));
|
||||
const auto& readFBO =
|
||||
MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
if (!readFBO) {
|
||||
@@ -11456,10 +11393,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// P5c (hd): with an active transport the handle is the caller's
|
||||
// (m_pushedSyncHandle), never the client allocator's - see SyncToBackend.
|
||||
const MG_Pipe::MGPipeHandle fbo =
|
||||
MG_Config::DataArmIsRecord()
|
||||
? m_pushedSyncHandle
|
||||
:
|
||||
g_backendFramebufferObjects.HandleOf(stateFBOObject.get());
|
||||
m_pushedSyncHandle;
|
||||
// ID-19: the OBJECT's record, not "the record of whatever is bound to READ". A
|
||||
// framebuffer whose read buffer is being pushed need not be the read binding at
|
||||
// all - glNamedFramebufferReadBuffer and the DSA clears reach here by name - and
|
||||
@@ -11632,10 +11566,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// is never probed (T2). A null there means a caller reached this arm without a
|
||||
// record, which the null-record refusal below names.
|
||||
const MG_Pipe::MGPipeHandle fbo =
|
||||
MG_Config::DataArmIsRecord()
|
||||
? m_pushedSyncHandle
|
||||
:
|
||||
g_backendFramebufferObjects.HandleOf(stateFBOObject.get());
|
||||
m_pushedSyncHandle;
|
||||
pushedRecord = PushedFramebufferRecord(fbo);
|
||||
if (pushedRecord == nullptr) {
|
||||
MGLOG_E_ONCE("MGPipe: framebuffer %u has no applier record on the handle arm, so it "
|
||||
@@ -12937,11 +12868,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// it - MGPImageView::InternalFormat is that same GLenum, copied by the client
|
||||
// out of the unit's own binding. A unit the applier has never been told about
|
||||
// answers 0, which is what an unbound unit answers on the frontend arm too.
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const auto& st = MG_Pipe::MGPipeApplier();
|
||||
if (static_cast<SizeT>(unit) >= st.BoundShaderImages.size()) return 0;
|
||||
return static_cast<Uint>(st.BoundShaderImages[static_cast<SizeT>(unit)].InternalFormat);
|
||||
}
|
||||
const auto& st = MG_Pipe::MGPipeApplier();
|
||||
if (static_cast<SizeT>(unit) >= st.BoundShaderImages.size()) return 0;
|
||||
return static_cast<Uint>(st.BoundShaderImages[static_cast<SizeT>(unit)].InternalFormat);
|
||||
return static_cast<Uint>(MG_Pipe::gPipeInputs.GetImageTextureBinding(unit).Format);
|
||||
}
|
||||
|
||||
|
||||
@@ -788,7 +788,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ProgramSubsystemEnabled() already latches, so this is one load and one test, and a latch
|
||||
// here would only hide which half answered.
|
||||
inline Bool ProgramHandleArm() {
|
||||
return MG_Config::DataArmIsRecord() && ProgramSubsystemEnabled();
|
||||
return ProgramSubsystemEnabled();
|
||||
}
|
||||
|
||||
// ---- P4a: what the twins read INSTEAD of the frontend object ----
|
||||
@@ -1312,8 +1312,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// conjunction in MultiDraw.cpp would satisfy in letter while giving the family two
|
||||
// selectors that can drift apart. One definition, spelled at every site that reads it.
|
||||
inline Bool VertexInputReadsRecords() {
|
||||
return MG_Config::DataArmIsRecord() &&
|
||||
VertexInputSubsystemEnabled();
|
||||
return VertexInputSubsystemEnabled();
|
||||
}
|
||||
|
||||
// Registered as the frontend's BufferBackendOps at backend init and on
|
||||
|
||||
@@ -289,15 +289,13 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// an empty slot, and not the arm test (ID-110: the arm was decided by the transport
|
||||
// above, never inferred from a null).
|
||||
Uint BoundDrawIndirectBufferId() {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return 0;
|
||||
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (resource == nullptr) {
|
||||
RefuseMissingIndirectBufferRecord("BoundDrawIndirectBufferId", res);
|
||||
}
|
||||
return resource->id;
|
||||
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return 0;
|
||||
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (resource == nullptr) {
|
||||
RefuseMissingIndirectBufferRecord("BoundDrawIndirectBufferId", res);
|
||||
}
|
||||
return resource->id;
|
||||
// MONOLITH GLUE from here down, token for token what this function did before.
|
||||
const auto& indirect =
|
||||
MG_Pipe::gPipeInputs.GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
|
||||
@@ -2334,9 +2334,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_Config::DataArmIsRecord()
|
||||
? SplitReadbackPackBuffer() :
|
||||
MG_Pipe::gPipeInputs.GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
SplitReadbackPackBuffer();
|
||||
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
|
||||
@@ -39,10 +39,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
namespace {
|
||||
void RejectWireLegacyBuffer() {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)");
|
||||
std::abort();
|
||||
}
|
||||
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
|
||||
@@ -395,19 +393,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
DrawWireIndirect(mode, type, indirect, drawcount, stride, true);
|
||||
return;
|
||||
}
|
||||
DrawWireIndirect(mode, type, indirect, drawcount, stride, true);
|
||||
return;
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::MultiDrawElementsIndirect called with null GL context");
|
||||
pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
||||
}
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
DrawWireIndirect(mode, 0, indirect, drawcount, stride, false);
|
||||
return;
|
||||
}
|
||||
DrawWireIndirect(mode, 0, indirect, drawcount, stride, false);
|
||||
return;
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::MultiDrawArraysIndirect called with null GL context");
|
||||
|
||||
@@ -459,20 +453,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
DrawWireIndirect(mode, type, indirect, maxdrawcount, stride, true, true, drawcount);
|
||||
return;
|
||||
}
|
||||
DrawWireIndirect(mode, type, indirect, maxdrawcount, stride, true, true, drawcount);
|
||||
return;
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirectCount called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::MultiDrawElementsIndirectCount called with null GL context");
|
||||
pVulkanRenderer->MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, maxdrawcount, stride);
|
||||
}
|
||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
DrawWireIndirect(mode, 0, indirect, maxdrawcount, stride, false, true, drawcount);
|
||||
return;
|
||||
}
|
||||
DrawWireIndirect(mode, 0, indirect, maxdrawcount, stride, false, true, drawcount);
|
||||
return;
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirectCount called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::MultiDrawArraysIndirectCount called with null GL context");
|
||||
|
||||
@@ -545,10 +535,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DrawElementsInstancedBaseVertexBaseInstance(mode, count, type, indices, instancecount, 0, 0);
|
||||
}
|
||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
DrawWireIndirect(mode, type, indirect, 1, 0, true);
|
||||
return;
|
||||
}
|
||||
DrawWireIndirect(mode, type, indirect, 1, 0, true);
|
||||
return;
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::DrawElementsIndirect called with null GL context");
|
||||
|
||||
@@ -608,10 +596,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DrawArraysInstancedBaseInstance(mode, first, count, instancecount, 0);
|
||||
}
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
DrawWireIndirect(mode, 0, indirect, 1, 0, false);
|
||||
return;
|
||||
}
|
||||
DrawWireIndirect(mode, 0, indirect, 1, 0, false);
|
||||
return;
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::DrawArraysIndirect called with null GL context");
|
||||
|
||||
@@ -684,10 +670,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// DispatchComputeIndirect takes its transport branch into DispatchWireComputeIndirect
|
||||
// (WireDraw.inc), which issues vkCmdDispatchIndirect from the verb's store. The CPU read
|
||||
// that was here (ReadWireBuffer, a whole-GPU wait once a shader had written the store) is gone.
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
pVulkanRenderer->DispatchComputeIndirect(indirect);
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->DispatchComputeIndirect(indirect);
|
||||
return;
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchComputeIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::DispatchComputeIndirect called with null GL context");
|
||||
pVulkanRenderer->DispatchComputeIndirect(indirect);
|
||||
@@ -749,23 +733,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
auto& state = MG_Pipe::MGPipeApplier();
|
||||
const auto handle = state.VerbStorageBlockProgram;
|
||||
if (handle.Slot >= state.ShaderCsos.size() || !state.ShaderCsos[handle.Slot].Live ||
|
||||
state.ShaderCsos[handle.Slot].Gen != handle.Gen || !storageBlockName) {
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:storage-block-program-record\"}");
|
||||
std::abort();
|
||||
}
|
||||
auto& record = state.ShaderCsos[handle.Slot];
|
||||
auto found = std::find_if(record.StorageOverrides.begin(), record.StorageOverrides.end(),
|
||||
[&](const auto& entry) { return entry.Name == storageBlockName; });
|
||||
if (found == record.StorageOverrides.end())
|
||||
record.StorageOverrides.push_back({storageBlockName, static_cast<Int32>(storageBlockBinding)});
|
||||
else found->Binding = static_cast<Int32>(storageBlockBinding);
|
||||
++record.BindingsSerial;
|
||||
return;
|
||||
auto& state = MG_Pipe::MGPipeApplier();
|
||||
const auto handle = state.VerbStorageBlockProgram;
|
||||
if (handle.Slot >= state.ShaderCsos.size() || !state.ShaderCsos[handle.Slot].Live ||
|
||||
state.ShaderCsos[handle.Slot].Gen != handle.Gen || !storageBlockName) {
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:storage-block-program-record\"}");
|
||||
std::abort();
|
||||
}
|
||||
auto& record = state.ShaderCsos[handle.Slot];
|
||||
auto found = std::find_if(record.StorageOverrides.begin(), record.StorageOverrides.end(),
|
||||
[&](const auto& entry) { return entry.Name == storageBlockName; });
|
||||
if (found == record.StorageOverrides.end())
|
||||
record.StorageOverrides.push_back({storageBlockName, static_cast<Int32>(storageBlockBinding)});
|
||||
else found->Binding = static_cast<Int32>(storageBlockBinding);
|
||||
++record.BindingsSerial;
|
||||
return;
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject || storageBlockName == nullptr) return;
|
||||
const Int maxBindings =
|
||||
@@ -852,28 +834,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// memory) into uint32 values with the loop-closing first index appended.
|
||||
static Bool BuildClosedLineLoopIndices(GLsizei count, GLenum type, const void* indices,
|
||||
Vector<Uint32>& outIndices) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const SizeT width = MG_Util::GetGLTypeSize(type);
|
||||
if ((width != 1 && width != 2 && width != 4) || count < 2) return false;
|
||||
const auto& bound = MG_Pipe::MGPipeApplier().IndexBuffer;
|
||||
Vector<Uint8> owned;
|
||||
const Uint8* bytes = static_cast<const Uint8*>(indices);
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(bound.Res)) {
|
||||
owned.resize(static_cast<SizeT>(count) * width);
|
||||
const Uint64 offset = bound.Offset + reinterpret_cast<Uint64>(indices);
|
||||
if (offset < bound.Offset || !pVulkanRenderer->GetWireBufferManager().ReadWireBuffer(
|
||||
bound.Res, offset, owned.size(), owned.data())) return false;
|
||||
bytes = owned.data();
|
||||
}
|
||||
if (!bytes) return false;
|
||||
outIndices.resize(static_cast<SizeT>(count) + 1);
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
outIndices[i] = 0;
|
||||
Memcpy(&outIndices[i], bytes + static_cast<SizeT>(i) * width, width);
|
||||
}
|
||||
outIndices[count] = outIndices[0];
|
||||
return true;
|
||||
const SizeT width = MG_Util::GetGLTypeSize(type);
|
||||
if ((width != 1 && width != 2 && width != 4) || count < 2) return false;
|
||||
const auto& bound = MG_Pipe::MGPipeApplier().IndexBuffer;
|
||||
Vector<Uint8> owned;
|
||||
const Uint8* bytes = static_cast<const Uint8*>(indices);
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(bound.Res)) {
|
||||
owned.resize(static_cast<SizeT>(count) * width);
|
||||
const Uint64 offset = bound.Offset + reinterpret_cast<Uint64>(indices);
|
||||
if (offset < bound.Offset || !pVulkanRenderer->GetWireBufferManager().ReadWireBuffer(
|
||||
bound.Res, offset, owned.size(), owned.data())) return false;
|
||||
bytes = owned.data();
|
||||
}
|
||||
if (!bytes) return false;
|
||||
outIndices.resize(static_cast<SizeT>(count) + 1);
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
outIndices[i] = 0;
|
||||
Memcpy(&outIndices[i], bytes + static_cast<SizeT>(i) * width, width);
|
||||
}
|
||||
outIndices[count] = outIndices[0];
|
||||
return true;
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0 || count < 2) {
|
||||
return false;
|
||||
@@ -959,23 +939,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// batch that command buffer belongs to - the handle SetupDraw is holding goes stale, and
|
||||
// the next vkCmd* records into a command buffer that is no longer the frame's. Reconciling
|
||||
// HERE, before SetupDraw starts recording, keeps the wait out of the recording.
|
||||
if (!MG_Config::DataArmIsRecord()) {
|
||||
const auto& currentVAO = MG_Pipe::gPipeInputs.GetBoundVertexArray();
|
||||
Bool clientArray = false;
|
||||
if (currentVAO) {
|
||||
for (const auto& attribute : currentVAO->GetAllAttributes()) {
|
||||
if (attribute.Enabled && !attribute.Buffer) {
|
||||
clientArray = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clientArray) {
|
||||
if (const auto& elementBuffer = currentVAO->GetIndexBufferBindingSlot().GetBoundObject()) {
|
||||
elementBuffer->SyncGpuWrites();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pVulkanRenderer->DrawElements(payload);
|
||||
}
|
||||
@@ -1028,9 +991,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// MultiDrawIndexedCmd left the client-memory shape addressing a view whose byte
|
||||
// offset is a hardcoded 0, so UploadAndBindIndexBuffer saw a null client pointer,
|
||||
// declined the whole batch and painted nothing.)
|
||||
const Bool noIndexBuffer = MG_Config::DataArmIsRecord()
|
||||
? MG_Pipe::MGPipeHandleIsNull(MG_Pipe::MGPipeApplier().IndexBuffer.Res)
|
||||
: MG_Pipe::gPipeInputs.GetBoundVertexArray()->GetIndexBufferBindingSlot().GetBoundObject() == nullptr;
|
||||
const Bool noIndexBuffer = MG_Pipe::MGPipeHandleIsNull(MG_Pipe::MGPipeApplier().IndexBuffer.Res);
|
||||
if (noIndexBuffer) {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) {
|
||||
|
||||
@@ -129,7 +129,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Split staging may also be the destination of a GPU UBO range copy.
|
||||
// Persistent host writes must be coherent: Upload deliberately writes
|
||||
// the mapped pointer without a per-range VMA flush.
|
||||
if (MG_Config::DataArmIsRecord() && m_desc.persistentlyMapped) {
|
||||
if (m_desc.persistentlyMapped) {
|
||||
bufferDesc.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
||||
}
|
||||
if (!buffer.Create(bufferDesc)) {
|
||||
|
||||
@@ -225,8 +225,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// A layout transition writes the image too. Order it after the wire
|
||||
// render pass's storeOp, clears and copies instead of relying on the old
|
||||
// per-draw queue-idle path. This applies even when contents are discarded.
|
||||
if (MG_Config::DataArmIsRecord())
|
||||
presentBarrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
presentBarrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
presentBarrier.dstAccessMask = 0;
|
||||
presentBarrier.oldLayout = oldLayout;
|
||||
presentBarrier.newLayout = presentLayout;
|
||||
@@ -239,8 +238,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
presentBarrier.subresourceRange.baseArrayLayer = 0;
|
||||
presentBarrier.subresourceRange.layerCount = 1;
|
||||
vkCmdPipelineBarrier(commandBuffer,
|
||||
MG_Config::DataArmIsRecord()
|
||||
? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
nullptr, 0, nullptr, 1, &presentBarrier);
|
||||
|
||||
|
||||
@@ -586,15 +586,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_bufferManager = bufferManager;
|
||||
m_programFactory = programFactory;
|
||||
m_minDynamicOffsetAlignment = std::max<VkDeviceSize>(1, minUniformBufferOffsetAlignment);
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
vkGetPhysicalDeviceProperties(m_physicalDevice, &properties);
|
||||
m_wireStorageOffsetAlignment = std::max<VkDeviceSize>(1, properties.limits.minStorageBufferOffsetAlignment);
|
||||
m_wireTexelOffsetAlignment = std::max<VkDeviceSize>(1, properties.limits.minTexelBufferOffsetAlignment);
|
||||
m_wireMaxUniformRange = properties.limits.maxUniformBufferRange;
|
||||
m_wireMaxStorageRange = properties.limits.maxStorageBufferRange;
|
||||
m_wireMaxTexelElements = properties.limits.maxTexelBufferElements;
|
||||
}
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
vkGetPhysicalDeviceProperties(m_physicalDevice, &properties);
|
||||
m_wireStorageOffsetAlignment = std::max<VkDeviceSize>(1, properties.limits.minStorageBufferOffsetAlignment);
|
||||
m_wireTexelOffsetAlignment = std::max<VkDeviceSize>(1, properties.limits.minTexelBufferOffsetAlignment);
|
||||
m_wireMaxUniformRange = properties.limits.maxUniformBufferRange;
|
||||
m_wireMaxStorageRange = properties.limits.maxStorageBufferRange;
|
||||
m_wireMaxTexelElements = properties.limits.maxTexelBufferElements;
|
||||
m_frameCount = frameCount;
|
||||
m_maxBindings = maxBindings;
|
||||
m_samplerResolveMemo.assign(m_maxBindings, SamplerResolveMemo{});
|
||||
|
||||
@@ -800,15 +800,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const auto found = m_wireVertexFormatSupport.find(format);
|
||||
if (found != m_wireVertexFormatSupport.end()) return found->second;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
const Bool supported = (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
m_wireVertexFormatSupport.emplace(format, supported);
|
||||
return supported;
|
||||
}
|
||||
const auto found = m_wireVertexFormatSupport.find(format);
|
||||
if (found != m_wireVertexFormatSupport.end()) return found->second;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
const Bool supported = (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
m_wireVertexFormatSupport.emplace(format, supported);
|
||||
return supported;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
|
||||
@@ -235,9 +235,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} // namespace
|
||||
|
||||
void VkBufferManager::RegisterWireResourceOps() {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
MG_Pipe::MGPipeSetResourceOps(&g_vulkanWireResourceOps);
|
||||
}
|
||||
MG_Pipe::MGPipeSetResourceOps(&g_vulkanWireResourceOps);
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -1383,7 +1381,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.frameCount = m_initInfo.frameCount,
|
||||
.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT |
|
||||
(MG_Config::DataArmIsRecord() ? VkBufferUsageFlags{VK_BUFFER_USAGE_TRANSFER_DST_BIT} : 0u) |
|
||||
(VkBufferUsageFlags{VK_BUFFER_USAGE_TRANSFER_DST_BIT}) |
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
||||
.memoryUsage = m_initInfo.transientMemoryUsage,
|
||||
.allocationFlags = m_initInfo.transientAllocationFlags,
|
||||
|
||||
@@ -3127,12 +3127,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
inline ProgramFactory::CompileOptionFlags GetShaderTransformFlags(VkSurfaceTransformFlagBitsKHR preTransform) {
|
||||
ProgramFactory::CompileOptionFlags flags = ProgramFactory::CompileOptionBit::PositionZRemap;
|
||||
const Bool wire = MG_Config::DataArmIsRecord();
|
||||
const auto* wireFbo = wire ? MG_Pipe::MGPipeApplier().DrawFramebuffer() : nullptr;
|
||||
const auto currentDrawFBO = wire ? SharedPtr<MG_State::GLState::FramebufferObject>{} :
|
||||
MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
const Bool isDefault = wire ? (wireFbo && wireFbo->IsDefault) :
|
||||
(currentDrawFBO && currentDrawFBO->IsDefaultFramebuffer());
|
||||
const auto* wireFbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
|
||||
const Bool isDefault = wireFbo && wireFbo->IsDefault;
|
||||
if (isDefault) {
|
||||
flags |= ProgramFactory::CompileOptionBit::PositionYFlip;
|
||||
// gl_FragCoord follows the same rule the default-framebuffer RECTANGLES follow
|
||||
@@ -5010,28 +5006,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// (stencil) test always passes and nothing is written - even when the bound
|
||||
// image is a packed depth-stencil texture attached through only one half.
|
||||
{
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
|
||||
if (fbo && !fbo->IsDefault) {
|
||||
if (fbo->Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) depthTestEnabled = false;
|
||||
if (fbo->Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) stencilTestEnabled = false;
|
||||
}
|
||||
} else
|
||||
{
|
||||
const auto& gatingFbo =
|
||||
MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (gatingFbo != nullptr && !gatingFbo->IsDefaultFramebuffer()) {
|
||||
const auto& depthAtt = gatingFbo->GetAttachment(MobileGL::FramebufferAttachmentType::Depth);
|
||||
const auto& stencilAtt = gatingFbo->GetAttachment(MobileGL::FramebufferAttachmentType::Stencil);
|
||||
if (!depthAtt.IsValid() || depthAtt.IsEmpty()) {
|
||||
depthTestEnabled = false;
|
||||
}
|
||||
if (!stencilAtt.IsValid() || stencilAtt.IsEmpty()) {
|
||||
stencilTestEnabled = false;
|
||||
}
|
||||
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
|
||||
if (fbo && !fbo->IsDefault) {
|
||||
if (fbo->Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) depthTestEnabled = false;
|
||||
if (fbo->Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) stencilTestEnabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
const StencilFaceState& frontStencil = MG_Pipe::gPipeInputs.GetStencilState(StencilFace::Front);
|
||||
const StencilFaceState& backStencil = MG_Pipe::gPipeInputs.GetStencilState(StencilFace::Back);
|
||||
const VkPolygonMode requestedPolygonMode =
|
||||
@@ -5240,8 +5220,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(payload.colorAttachmentCount <= PipelineFactory::PipelineCreatePayload::kMaxColorAttachments,
|
||||
"GetOrCreatePipeline: colorAttachmentCount=%u exceeds payload capacity",
|
||||
payload.colorAttachmentCount);
|
||||
const auto* wireFbo = MG_Config::DataArmIsRecord() ?
|
||||
MG_Pipe::MGPipeApplier().DrawFramebuffer() : nullptr;
|
||||
const auto* wireFbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
|
||||
const auto drawFboBinding = wireFbo ? SharedPtr<MG_State::GLState::FramebufferObject>{} :
|
||||
MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
MOBILEGL_ASSERT(wireFbo != nullptr || drawFboBinding != nullptr, "GetOrCreatePipeline: draw framebuffer is null");
|
||||
@@ -5903,10 +5882,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the SAME draw-framebuffer binding the draw uses - see the assert below.
|
||||
MOBILEGL_ASSERT(
|
||||
[&] {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
|
||||
return isDefaultFbo == (fbo != nullptr && fbo->IsDefault);
|
||||
}
|
||||
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
|
||||
return isDefaultFbo == (fbo != nullptr && fbo->IsDefault);
|
||||
const auto& fbo =
|
||||
MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
return isDefaultFbo == (fbo != nullptr && fbo->IsDefaultFramebuffer());
|
||||
@@ -6332,14 +6309,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// split server records every draw its client sends into the same frame command buffer, so
|
||||
// the same loading frame grows it the same way there.
|
||||
SplitOversizedRecording();
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
if (!RewindWireDescriptorSetsIfDue()) return false;
|
||||
// The wire route returns before the monolith branch's draw-gated
|
||||
// sweep. Dead wire texture/renderbuffer records otherwise live
|
||||
// until the next frame boundary, which a long trace may not reach.
|
||||
m_textureManager->CollectGarbage();
|
||||
return SetupWireDraw(frame, mode, aspects, drawParams, pIndexBufferView);
|
||||
}
|
||||
if (!RewindWireDescriptorSetsIfDue()) return false;
|
||||
// The wire route returns before the monolith branch's draw-gated
|
||||
// sweep. Dead wire texture/renderbuffer records otherwise live
|
||||
// until the next frame boundary, which a long trace may not reach.
|
||||
m_textureManager->CollectGarbage();
|
||||
return SetupWireDraw(frame, mode, aspects, drawParams, pIndexBufferView);
|
||||
// Sync each sampled texture at most once across this whole draw: the layout
|
||||
// probe loop, the post-transition loop, and ResolveSamplerDescriptor would
|
||||
// otherwise each re-run the full SyncTexture path on the same textures.
|
||||
@@ -6965,12 +6940,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
|
||||
// Before the wire branch, for SetupDraw's reason.
|
||||
SplitOversizedRecording();
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
if (!RewindWireDescriptorSetsIfDue()) return;
|
||||
m_textureManager->CollectGarbage();
|
||||
DispatchWireCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||
return;
|
||||
}
|
||||
if (!RewindWireDescriptorSetsIfDue()) return;
|
||||
m_textureManager->CollectGarbage();
|
||||
DispatchWireCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||
return;
|
||||
m_textureManager->CollectGarbage();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
// The DISPATCH accessor: with a pipeline bound this is its compute stage program
|
||||
@@ -7029,12 +7002,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) {
|
||||
SplitOversizedRecording();
|
||||
// P8-SV: the wire arm's own indirect dispatch, DispatchCompute's shape one call over.
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
if (!RewindWireDescriptorSetsIfDue()) return;
|
||||
m_textureManager->CollectGarbage();
|
||||
DispatchWireComputeIndirect(indirect);
|
||||
return;
|
||||
}
|
||||
if (!RewindWireDescriptorSetsIfDue()) return;
|
||||
m_textureManager->CollectGarbage();
|
||||
DispatchWireComputeIndirect(indirect);
|
||||
return;
|
||||
m_textureManager->CollectGarbage();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
// See DispatchCompute: the dispatch accessor, not the draw one.
|
||||
@@ -7139,9 +7110,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vkCmdPipelineBarrier(frame.commandBuffer,
|
||||
// ALL_COMMANDS does not include HOST. The barrier's
|
||||
// HOST_WRITE access must have a matching source stage.
|
||||
MG_Config::DataArmIsRecord()
|
||||
? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT
|
||||
: VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0,
|
||||
1, &memoryBarrier, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
@@ -7301,17 +7270,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#include "WireYuvImage.inc"
|
||||
|
||||
void VulkanRenderer::Clear(GLbitfield mask) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
|
||||
if (!fbo) MagmaWireFatal("clear-framebuffer-record");
|
||||
ClearAttachmentPayload payload{};
|
||||
payload.mask = mask;
|
||||
payload.color = MG_Pipe::gPipeInputs.GetClearColor();
|
||||
payload.depth = MG_Pipe::gPipeInputs.GetClearDepth();
|
||||
payload.stencil = MG_Pipe::gPipeInputs.GetClearStencil();
|
||||
ClearWireFramebuffer(*fbo, payload);
|
||||
return;
|
||||
}
|
||||
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
|
||||
if (!fbo) MagmaWireFatal("clear-framebuffer-record");
|
||||
ClearAttachmentPayload payload{};
|
||||
payload.mask = mask;
|
||||
payload.color = MG_Pipe::gPipeInputs.GetClearColor();
|
||||
payload.depth = MG_Pipe::gPipeInputs.GetClearDepth();
|
||||
payload.stencil = MG_Pipe::gPipeInputs.GetClearStencil();
|
||||
ClearWireFramebuffer(*fbo, payload);
|
||||
return;
|
||||
m_clearManager->CollectGarbage();
|
||||
if ((mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) {
|
||||
return;
|
||||
@@ -7703,12 +7670,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer,
|
||||
const ClearAttachmentPayload& clearPayload) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
|
||||
if (!fbo) MagmaWireFatal("clear-buffer-framebuffer-record");
|
||||
ClearWireFramebuffer(*fbo,clearPayload,buffer == GL_COLOR ? drawbuffer : -1);
|
||||
return;
|
||||
}
|
||||
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
|
||||
if (!fbo) MagmaWireFatal("clear-buffer-framebuffer-record");
|
||||
ClearWireFramebuffer(*fbo,clearPayload,buffer == GL_COLOR ? drawbuffer : -1);
|
||||
return;
|
||||
auto* fbo = MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
|
||||
if (!fbo) {
|
||||
return;
|
||||
@@ -8727,10 +8692,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VulkanRenderer::BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLbitfield mask, GLenum filter) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
BlitWireFramebuffers(srcX0,srcY0,srcX1,srcY1,dstX0,dstY0,dstX1,dstY1,mask,filter);
|
||||
return;
|
||||
}
|
||||
BlitWireFramebuffers(srcX0,srcY0,srcX1,srcY1,dstX0,dstY0,dstX1,dstY1,mask,filter);
|
||||
return;
|
||||
auto readFbo = MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
auto drawFbo = MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||
@@ -9367,10 +9330,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void VulkanRenderer::CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
||||
GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
CopyWireFramebufferToTexture(target,level,xoffset,yoffset,x,y,width,height);
|
||||
return;
|
||||
}
|
||||
CopyWireFramebufferToTexture(target,level,xoffset,yoffset,x,y,width,height);
|
||||
return;
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -9684,8 +9645,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
Uint CopyImageEndpointName(const CopyImageEndpoint& endpoint) {
|
||||
if (MG_Config::DataArmIsRecord())
|
||||
return endpoint.IsRenderbuffer() ? endpoint.RenderbufferHandle.Slot : endpoint.TextureHandle.Slot;
|
||||
return endpoint.IsRenderbuffer() ? endpoint.RenderbufferHandle.Slot : endpoint.TextureHandle.Slot;
|
||||
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetExternalIndex();
|
||||
return endpoint.Texture ? endpoint.Texture->GetExternalIndex() : 0u;
|
||||
}
|
||||
@@ -9696,9 +9656,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const CopyImageEndpoint& dstEndpoint,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
const Bool wire = MG_Config::DataArmIsRecord();
|
||||
const Bool wire = true; // P13 W6: the record arm is the only arm
|
||||
MG_Pipe::MGPipeHandle dstStorageHandle = dstEndpoint.TextureHandle;
|
||||
if (wire && HasPendingRecordedWork() && !FlushPendingCommands()) {
|
||||
if (HasPendingRecordedWork() && !FlushPendingCommands()) {
|
||||
if (LatchWireDeviceLoss("copy-image-flush")) return;
|
||||
MagmaWireFatal("copy-image-flush");
|
||||
}
|
||||
@@ -9740,24 +9700,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the
|
||||
// render-pass manager, which is where a renderbuffer's VkImage lives.
|
||||
const auto resolveImage = [this](const CopyImageEndpoint& endpoint, CopyImageVkImage& out) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
const Bool renderbuffer = endpoint.IsRenderbuffer();
|
||||
// The same handle-keyed allocation used by wire FBO clear/draw/read.
|
||||
// The legacy render-pass manager owns a different, frontend-keyed map.
|
||||
auto* resource = m_textureManager->SyncTextureResourceByHandle(
|
||||
renderbuffer ? endpoint.RenderbufferHandle : endpoint.TextureHandle, renderbuffer);
|
||||
if (!resource) MagmaWireFatal("copy-image-resource");
|
||||
out.isRenderbuffer = renderbuffer;
|
||||
out.image = resource->image;
|
||||
out.trackedLayout = &resource->layout;
|
||||
out.aspect = resource->aspect;
|
||||
out.mipLevels = resource->mipLevels;
|
||||
out.extent = resource->extent;
|
||||
out.depth = resource->depth;
|
||||
out.arrayLayers = resource->arrayLayers;
|
||||
out.format = resource->format;
|
||||
return out.image != VK_NULL_HANDLE;
|
||||
}
|
||||
const Bool renderbuffer = endpoint.IsRenderbuffer();
|
||||
// The same handle-keyed allocation used by wire FBO clear/draw/read.
|
||||
// The legacy render-pass manager owns a different, frontend-keyed map.
|
||||
auto* resource = m_textureManager->SyncTextureResourceByHandle(
|
||||
renderbuffer ? endpoint.RenderbufferHandle : endpoint.TextureHandle, renderbuffer);
|
||||
if (!resource) MagmaWireFatal("copy-image-resource");
|
||||
out.isRenderbuffer = renderbuffer;
|
||||
out.image = resource->image;
|
||||
out.trackedLayout = &resource->layout;
|
||||
out.aspect = resource->aspect;
|
||||
out.mipLevels = resource->mipLevels;
|
||||
out.extent = resource->extent;
|
||||
out.depth = resource->depth;
|
||||
out.arrayLayers = resource->arrayLayers;
|
||||
out.format = resource->format;
|
||||
return out.image != VK_NULL_HANDLE;
|
||||
if (endpoint.IsRenderbuffer()) {
|
||||
auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(endpoint.Renderbuffer);
|
||||
if (resource == nullptr) return false;
|
||||
@@ -9988,7 +9946,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) {
|
||||
if (MG_Config::DataArmIsRecord()) return true;
|
||||
return true;
|
||||
if (endpoint.IsRenderbuffer()) {
|
||||
return MaterializePendingClearForRenderbuffer(frame.commandBuffer, endpoint.Renderbuffer);
|
||||
}
|
||||
@@ -10260,10 +10218,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void VulkanRenderer::ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
ReadWirePixels(x,y,width,height,format,type,pixels);
|
||||
return;
|
||||
}
|
||||
ReadWirePixels(x,y,width,height,format,type,pixels);
|
||||
return;
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -10958,8 +10914,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Store honoring the client pack state (single slice).
|
||||
const SharedPtr<MG_State::GLState::BufferObject> wireReplyHasNoPackBuffer;
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_Config::DataArmIsRecord() ? wireReplyHasNoPackBuffer :
|
||||
MG_Pipe::gPipeInputs.GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
wireReplyHasNoPackBuffer;
|
||||
const auto packParams = MG_Pipe::gPipeInputs.GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT packAlignment = packParams.Alignment > 0 ? static_cast<SizeT>(packParams.Alignment) : 1;
|
||||
@@ -11234,10 +11189,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VulkanRenderer::GenerateMipmap(GLenum target) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
GenerateWireMipmap();
|
||||
return;
|
||||
}
|
||||
GenerateWireMipmap();
|
||||
return;
|
||||
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
// Whatever is left here is a coverage gap in this backend, not a broken invariant, so it
|
||||
// declines (leaving the mip chain unwritten) rather than asserting the process down. What
|
||||
@@ -11341,9 +11294,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// server's staged shadow (keyed by the synced TextureResource above) and the client's
|
||||
// level storage is never written; in monolith the client-object path runs unchanged.
|
||||
const Bool allocatedMipmapStorage =
|
||||
MG_Config::DataArmIsRecord()
|
||||
? EnsureGenerateMipmapShadowAllocated(*resource, baseMipLevel, uploadTargets)
|
||||
: EnsureGenerateMipmapStorageAllocated(*mipmapTexture, baseMipLevel, endMipLevel);
|
||||
EnsureGenerateMipmapShadowAllocated(*resource, baseMipLevel, uploadTargets);
|
||||
MOBILEGL_ASSERT(allocatedMipmapStorage, "GenerateMipmap could not allocate a full mip chain for this texture.");
|
||||
|
||||
resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
||||
@@ -11632,8 +11583,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"undefined behaviour rather than a capture");
|
||||
return false;
|
||||
}
|
||||
if (MG_Config::DataArmIsRecord())
|
||||
return BeginWireXfbCaptureForDraw(frame);
|
||||
return BeginWireXfbCaptureForDraw(frame);
|
||||
const auto& program = MG_Pipe::gPipeInputs.GetTransformFeedbackProgram();
|
||||
if (!program || program->GetTransformFeedbackVaryingCount() == 0) {
|
||||
return false;
|
||||
@@ -12653,42 +12603,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vertexRange.instanceCount = 1;
|
||||
// MONOLITH ONLY: with a transport the frontend VAO is not this side's to read at all
|
||||
// (the client owns those bytes and stages them itself), and this runs on the apply thread.
|
||||
if (!MG_Config::DataArmIsRecord()) {
|
||||
const auto& currentVAO = MG_Pipe::gPipeInputs.GetBoundVertexArray();
|
||||
Bool clientArray = false;
|
||||
if (currentVAO) {
|
||||
for (const auto& attribute : currentVAO->GetAllAttributes()) {
|
||||
if (attribute.Enabled && !attribute.Buffer) {
|
||||
clientArray = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clientArray) {
|
||||
// The commands may be shader-written, and a stale shadow would size the upload
|
||||
// off words no shader ever wrote.
|
||||
drawBuffer->SyncGpuWrites();
|
||||
const Uint8* commandHostBytes = drawBuffer->MappedData();
|
||||
if (commandHostBytes == nullptr) {
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: a client-memory vertex array needs "
|
||||
"the commands on the CPU and the indirect buffer has no readable shadow");
|
||||
return;
|
||||
}
|
||||
Uint64 lastElement = 0;
|
||||
for (GLsizei idraw = 0; idraw < drawcount; ++idraw) {
|
||||
VkDrawIndirectCommand command{};
|
||||
Memcpy(&command,
|
||||
commandHostBytes + commandOffset + static_cast<SizeT>(idraw) * static_cast<SizeT>(stride),
|
||||
sizeof(command));
|
||||
lastElement = std::max<Uint64>(lastElement,
|
||||
static_cast<Uint64>(command.firstVertex) + command.vertexCount);
|
||||
vertexRange.firstInstance = std::max(vertexRange.firstInstance, command.firstInstance);
|
||||
vertexRange.instanceCount = std::max(vertexRange.instanceCount, command.instanceCount);
|
||||
}
|
||||
vertexRange.firstVertex = 0;
|
||||
vertexRange.vertexCount = static_cast<Uint32>(lastElement);
|
||||
}
|
||||
}
|
||||
|
||||
if (!SetupDraw(frame, mode, DrawSetupAspect::IndirectDrawBuffer, vertexRange)) {
|
||||
return;
|
||||
@@ -12997,16 +12911,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
// Texture uploads submit on this queue with their own fences. The
|
||||
// renderer watermark alone cannot prove images/views are idle.
|
||||
if (m_textureManager && !m_textureManager->WireUploadsAreIdle()) return false;
|
||||
// No preparation, recording or GPU work remains. A minimized Present
|
||||
// may have abandoned a recording tagged for a submission that will
|
||||
// never occur, so reclaim those future-tagged objects as well.
|
||||
CollectWireObjects(m_completedSubmitCounter, true);
|
||||
ClearAllWireDrawPassCaches();
|
||||
}
|
||||
// Texture uploads submit on this queue with their own fences. The
|
||||
// renderer watermark alone cannot prove images/views are idle.
|
||||
if (m_textureManager && !m_textureManager->WireUploadsAreIdle()) return false;
|
||||
// No preparation, recording or GPU work remains. A minimized Present
|
||||
// may have abandoned a recording tagged for a submission that will
|
||||
// never occur, so reclaim those future-tagged objects as well.
|
||||
CollectWireObjects(m_completedSubmitCounter, true);
|
||||
ClearAllWireDrawPassCaches();
|
||||
|
||||
// Every submission is complete and nothing recorded references the
|
||||
// per-frame transients. Pure-reclaim work runs on every drain: it only
|
||||
@@ -14420,11 +14332,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPhysicalDeviceDescriptorIndexingProperties descriptorIndexingProperties{};
|
||||
descriptorIndexingProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES;
|
||||
const Bool descriptorIndexingCore =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && !defined(VK_USE_PLATFORM_WIN32_KHR)
|
||||
#if !defined(VK_USE_PLATFORM_WIN32_KHR)
|
||||
// CreateInstance requests Vulkan 1.1 on these platforms. A 1.2+ GPU
|
||||
// does not promote descriptor indexing into that application's core
|
||||
// API; the wire path must enable VK_EXT_descriptor_indexing instead.
|
||||
(!MG_Config::DataArmIsRecord()) &&
|
||||
// API, so the record arm enables VK_EXT_descriptor_indexing instead (P13 W6:
|
||||
// every library, not only the transport build - the old monolith-only `true`
|
||||
// here treated the 1.2 device as core under a 1.1 instance).
|
||||
false &&
|
||||
#endif
|
||||
m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_2;
|
||||
const Bool descriptorIndexingExtension =
|
||||
@@ -14446,8 +14360,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_1 &&
|
||||
IsExtensionSupported(availableExtensions, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME) &&
|
||||
IsExtensionSupported(availableExtensions, VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME);
|
||||
const Bool wireDepthResolveEnabled = MG_Config::DataArmIsRecord() &&
|
||||
getPhysicalDeviceProperties2 && (wireDepthResolveCore || wireDepthResolveExtensions);
|
||||
const Bool wireDepthResolveEnabled = getPhysicalDeviceProperties2 && (wireDepthResolveCore || wireDepthResolveExtensions);
|
||||
m_wireCreateRenderPass2 = nullptr;
|
||||
m_wireDepthResolveModes = m_wireStencilResolveModes = 0;
|
||||
if (wireDepthResolveEnabled) {
|
||||
@@ -14470,8 +14383,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// write the stencil aspect without it. Enabled on the same terms as the resolve
|
||||
// extensions above - wire arms only - and the arm declines stencil where it is absent
|
||||
// rather than producing an undefined aspect.
|
||||
m_wireShaderStencilExport = MG_Config::DataArmIsRecord() &&
|
||||
IsExtensionSupported(availableExtensions, VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
|
||||
m_wireShaderStencilExport = IsExtensionSupported(availableExtensions, VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
|
||||
if (m_wireShaderStencilExport)
|
||||
EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions,
|
||||
VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
|
||||
@@ -14487,8 +14399,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_wireGetAhbProperties = nullptr;
|
||||
m_sharedImageSyncFd = false;
|
||||
#if defined(__ANDROID__)
|
||||
if (MG_Config::DataArmIsRecord() &&
|
||||
m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_1 &&
|
||||
if (m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_1 &&
|
||||
IsExtensionSupported(availableExtensions, VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME) &&
|
||||
IsExtensionSupported(availableExtensions, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME)) {
|
||||
m_wireAhbImport = true;
|
||||
@@ -15038,7 +14949,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
IsExtensionSupported(availableExtensions, VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) ? VK_EXT_ROBUSTNESS_2_EXTENSION_NAME
|
||||
: IsExtensionSupported(availableExtensions, VK_KHR_ROBUSTNESS_2_EXTENSION_NAME) ? VK_KHR_ROBUSTNESS_2_EXTENSION_NAME
|
||||
: nullptr;
|
||||
if (MG_Config::DataArmIsRecord() && robustness2Name != nullptr &&
|
||||
if (robustness2Name != nullptr &&
|
||||
getPhysicalDeviceFeatures2 != nullptr) {
|
||||
VkPhysicalDeviceFeatures2 featureQuery{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
|
||||
featureQuery.pNext = &robustness2Features;
|
||||
@@ -15052,10 +14963,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_wireNullDescriptor = true;
|
||||
}
|
||||
}
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
MGLOG_I("Magma wire: robustness2 nullDescriptor %s (%s)", m_wireNullDescriptor ? "enabled" : "unavailable",
|
||||
robustness2Name != nullptr ? robustness2Name : "no robustness2 extension");
|
||||
}
|
||||
MGLOG_I("Magma wire: robustness2 nullDescriptor %s (%s)", m_wireNullDescriptor ? "enabled" : "unavailable",
|
||||
robustness2Name != nullptr ? robustness2Name : "no robustness2 extension");
|
||||
|
||||
deviceCreateInfo.enabledExtensionCount = static_cast<Uint32>(enabledDeviceExtensions.size());
|
||||
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
|
||||
@@ -15285,7 +15194,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// red when the real recording, readback or tally stops detecting it too.
|
||||
void VulkanRenderer::ArmWireDepthResolveOrder() {
|
||||
m_wirePreferShaderDepthResolve = false;
|
||||
if (!MG_Config::DataArmIsRecord()) return;
|
||||
// ResolveWireDepthStencil's own test for the render-pass arm, less its per-format half
|
||||
// (the probe asks that per format): without it the shader pass is the only arm.
|
||||
const Bool renderPassArmAvailable = !MagmaWireForcedShaderDepthResolve() && m_wireCreateRenderPass2 &&
|
||||
|
||||
@@ -1341,7 +1341,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// BeginXfbCaptureForDraw does mark the capture targets - where the fence at the
|
||||
// caller still runs, so the readback it emits is pure new work on the monolith
|
||||
// path and integration-gpu cannot see it.
|
||||
if (MG_Config::DataArmIsRecord()) buffer->SyncGpuWrites();
|
||||
buffer->SyncGpuWrites();
|
||||
const Range1D range = bindingPoint.GetRange();
|
||||
const Uint8* mapped = buffer->MappedData();
|
||||
if (mapped == nullptr) continue;
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
// SyncGpuWrites, exactly as glMapBufferRange and glGetBufferSubData already do. Monolith keeps its
|
||||
// read as it was: its ReadPixels maps the driver PBO back into the shadow inside the call, and the
|
||||
// pull build compiles this to nothing (G1).
|
||||
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) do { if (MG_Config::DataArmIsRecord()) (buffer)->SyncGpuWrites(); } while (0)
|
||||
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) do { (buffer)->SyncGpuWrites(); } while (0)
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
|
||||
@@ -6117,8 +6117,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
viewObject->SetInternalFormat(viewInternalFormat);
|
||||
viewObject->SetSamples(storageOwner->GetSamples());
|
||||
viewObject->SetFixedSampleLocations(storageOwner->HasFixedSampleLocations());
|
||||
if (MG_Config::DataArmIsRecord())
|
||||
MG_Pipe::MGPipeEmitSamplerViewCreate(*viewObject);
|
||||
MG_Pipe::MGPipeEmitSamplerViewCreate(*viewObject);
|
||||
}
|
||||
|
||||
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) {
|
||||
@@ -6580,12 +6579,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static void GetTextureImageForUploadTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level, GLenum format,
|
||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
MGP_FILL(GetTextureImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
}
|
||||
MGP_FILL(GetTextureImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
||||
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
||||
MGL_BACKEND_SLOT_LOCAL(GetTextureImage)) {
|
||||
@@ -6607,8 +6604,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
auto uploadTarget = GetPrimaryUploadTarget(textureObject);
|
||||
// Unlike glGetTexImage's face target, this DSA call reads all six faces.
|
||||
if (MG_Config::DataArmIsRecord() &&
|
||||
textureObject->GetTarget() == TextureTarget::TextureCubeMap)
|
||||
if (textureObject->GetTarget() == TextureTarget::TextureCubeMap)
|
||||
uploadTarget = TextureUploadTarget::Unknown;
|
||||
GetTextureImageForUploadTarget(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels, __func__);
|
||||
@@ -6870,10 +6866,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
if (!GetTexImage_State(target, level, format, type, pixels)) return;
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
GetTexImage_Backend(target, level, format, type, pixels);
|
||||
return;
|
||||
}
|
||||
GetTexImage_Backend(target, level, format, type, pixels);
|
||||
return;
|
||||
if (MGL_BACKEND_SLOT_LOCAL(GetTexImage)) {
|
||||
GetTexImage_Backend(target, level, format, type, pixels);
|
||||
return;
|
||||
|
||||
@@ -2262,9 +2262,7 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
Bool PipeInputs::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
|
||||
if (MG_Config::DataArmIsRecord()) {
|
||||
return lifetimeId != 0 && MGPipeApplier().StreamOutputSpans.count(lifetimeId) != 0;
|
||||
}
|
||||
return lifetimeId != 0 && MGPipeApplier().StreamOutputSpans.count(lifetimeId) != 0;
|
||||
|
||||
MGP_STICKY_FORWARD_PULL(HasOpenTransformFeedbackSpan);
|
||||
const auto* ctx = LiveContext();
|
||||
|
||||
@@ -384,13 +384,11 @@ namespace MobileGL::MG_Pipe {
|
||||
vao.GetAttributeBindingIndex(static_cast<Uint>(i)));
|
||||
// Client addresses never cross the transport. Draw emission snapshots
|
||||
// their referenced elements into owned buffers with a zero byte origin.
|
||||
if (MG_Config::DataArmIsRecord() &&
|
||||
!vao.GetAttribute(static_cast<Uint>(i)).Buffer) m_attributes[i].Offset = 0;
|
||||
if (!vao.GetAttribute(static_cast<Uint>(i)).Buffer) m_attributes[i].Offset = 0;
|
||||
}
|
||||
for (SizeT i = 0; i < kBindings; ++i) {
|
||||
m_bindingPoints[i] = MGPipeBuildVertexBindingPointWire(vao.GetBindingPoint(static_cast<Uint>(i)));
|
||||
if (MG_Config::DataArmIsRecord() &&
|
||||
!vao.GetBindingPoint(static_cast<Uint>(i)).Buffer) m_bindingPoints[i].Offset = 0;
|
||||
if (!vao.GetBindingPoint(static_cast<Uint>(i)).Buffer) m_bindingPoints[i].Offset = 0;
|
||||
}
|
||||
// Attributes first, then binding points, both ascending and contiguous.
|
||||
constexpr SizeT kAttribBytes = kAttribs * sizeof(MGPVertexAttribWire);
|
||||
|
||||
@@ -73,6 +73,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/AsyncCompileScenario.cpp
|
||||
Scenarios/ArbShaderObjectsScenario.cpp
|
||||
Scenarios/CubeFaceReadbackScenario.cpp
|
||||
Scenarios/TextureUnitSamplingScenario.cpp
|
||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||
Scenarios/UnwrittenPositionOutputScenario.cpp
|
||||
Scenarios/SampleMaskScopeScenario.cpp
|
||||
@@ -2456,6 +2457,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
mgl_itest_register_split_arms("ArbShaderObjectsScenario.*" "")
|
||||
# P13 W6: the cube-face readback rules the deleted frontend-arm unit cases pinned.
|
||||
mgl_itest_register_split_arms("CubeFaceReadbackScenario.*" "")
|
||||
mgl_itest_register_split_arms("TextureUnitSamplingScenario.*" "")
|
||||
mgl_itest_register_split_arms("TriangleScenario.*" "")
|
||||
mgl_itest_register_split_arms("PersistentCoherentMapScenario.*" "")
|
||||
# P11 A2 (MG_Remote/CONTRACT-P11.md §2): the >= 16 MiB arena on all three arms. Its tier case
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureUnitSamplingScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// P13 W6: WHAT A DRAW SAMPLES FROM A TEXTURE UNIT, ON A REAL CONTEXT. Two GL 4.6 rules that were
|
||||
// pinned by DirectGLESSanity unit cases driving the monolith frontend arm's unit walk
|
||||
// (BindCurrentTextures over the frontend's binding slots). That arm is gone - every library walks
|
||||
// the units by handle now - so the rules are asserted here, through the whole pipe, on both
|
||||
// backends and every arm the scenarios run on:
|
||||
//
|
||||
// * A multisample texture is fetched, never filtered, so the mip-chain completeness rules do
|
||||
// not apply to it (GL 4.6 8.17) even though its MIN_FILTER keeps the initial
|
||||
// NEAREST_MIPMAP_LINEAR. Calling it incomplete leaves the unit unbound, and every texelFetch
|
||||
// reads zero from a texture that was written correctly (KHR-GL43.compute_shader.resource-
|
||||
// texture's shape: clear through an FBO, then read through sampler2DMS).
|
||||
// * Binding texture 0 to a unit samples the unit's default texture, which has no image and is
|
||||
// therefore incomplete: (0, 0, 0, 1) (GL 4.6 8.17.1). The texture bound there before must not
|
||||
// keep answering.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr GLsizei kEdge = 4;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFetchMultisampleSource = R"(#version 330 core
|
||||
uniform sampler2DMS u_texture;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy), 0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kSample2DSource = R"(#version 330 core
|
||||
uniform sampler2D u_texture;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = texture(u_texture, vec2(0.5, 0.5)); }
|
||||
)";
|
||||
|
||||
class TextureUnitSamplingScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
DrainErrors();
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenTextures(1, &m_target);
|
||||
glBindTexture(GL_TEXTURE_2D, m_target);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_target, 0);
|
||||
glViewport(0, 0, kEdge, kEdge);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_msFbo != 0) glDeleteFramebuffers(1, &m_msFbo);
|
||||
if (m_target != 0) glDeleteTextures(1, &m_target);
|
||||
if (m_source != 0) glDeleteTextures(1, &m_source);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DrainErrors();
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static GLuint Compile(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint ok = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (ok != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||
ADD_FAILURE() << "shader compile failed: " << log;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
void UseProgram(const char* fragmentSource) {
|
||||
const GLuint vs = Compile(GL_VERTEX_SHADER, kVertexSource);
|
||||
const GLuint fs = Compile(GL_FRAGMENT_SHADER, fragmentSource);
|
||||
m_program = glCreateProgram();
|
||||
glAttachShader(m_program, vs);
|
||||
glAttachShader(m_program, fs);
|
||||
glLinkProgram(m_program);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
GLint ok = GL_FALSE;
|
||||
glGetProgramiv(m_program, GL_LINK_STATUS, &ok);
|
||||
ASSERT_EQ(ok, GL_TRUE) << "program link failed";
|
||||
glUseProgram(m_program);
|
||||
glUniform1i(glGetUniformLocation(m_program, "u_texture"), 0);
|
||||
}
|
||||
|
||||
std::array<GLubyte, 4> DrawAndReadCentre() {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glClearColor(0.5f, 0.5f, 0.5f, 0.5f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
std::array<GLubyte, 4> texel{};
|
||||
glReadPixels(kEdge / 2, kEdge / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, texel.data());
|
||||
return texel;
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_msFbo = 0;
|
||||
GLuint m_target = 0;
|
||||
GLuint m_source = 0;
|
||||
GLuint m_program = 0;
|
||||
};
|
||||
|
||||
TEST_F(TextureUnitSamplingScenario, AMultisampleTextureIsFetchedDespiteItsDefaultMipmapFilter) {
|
||||
if (!Ready()) return;
|
||||
glGenTextures(1, &m_source);
|
||||
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, m_source);
|
||||
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 4, GL_RGBA8, kEdge, kEdge, GL_FALSE);
|
||||
GLint minFilter = 0;
|
||||
glGetTexParameteriv(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_MIN_FILTER, &minFilter);
|
||||
ASSERT_EQ(minFilter, GL_NEAREST_MIPMAP_LINEAR)
|
||||
<< "the fixture is stale: the initial MIN_FILTER no longer asks for mipmaps";
|
||||
|
||||
glGenFramebuffers(1, &m_msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_msFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, m_source, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
glClearColor(1.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "clearing the multisample texture failed";
|
||||
|
||||
UseProgram(kFetchMultisampleSource);
|
||||
if (HasFatalFailure()) return;
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, m_source);
|
||||
const auto texel = DrawAndReadCentre();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_EQ(static_cast<int>(texel[0]), 255) << "red: the multisample texture was not bound";
|
||||
EXPECT_EQ(static_cast<int>(texel[1]), 0);
|
||||
EXPECT_EQ(static_cast<int>(texel[2]), 255) << "blue: the multisample texture was not bound";
|
||||
EXPECT_EQ(static_cast<int>(texel[3]), 255);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
TEST_F(TextureUnitSamplingScenario, BindingZeroSamplesTheIncompleteDefaultTexture) {
|
||||
if (!Ready()) return;
|
||||
glGenTextures(1, &m_source);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_source);
|
||||
const GLubyte red[4] = {255, 0, 0, 255};
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, red);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
UseProgram(kSample2DSource);
|
||||
if (HasFatalFailure()) return;
|
||||
|
||||
const auto bound = DrawAndReadCentre();
|
||||
ASSERT_EQ(static_cast<int>(bound[0]), 255) << "the control: the bound texture is sampled";
|
||||
ASSERT_EQ(static_cast<int>(bound[1]), 0);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
const auto unbound = DrawAndReadCentre();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_EQ(static_cast<int>(unbound[0]), 0)
|
||||
<< "texture 0 is bound, yet the unit still answers with the texture bound before it";
|
||||
EXPECT_EQ(static_cast<int>(unbound[1]), 0);
|
||||
EXPECT_EQ(static_cast<int>(unbound[2]), 0);
|
||||
EXPECT_EQ(static_cast<int>(unbound[3]), 255) << "an incomplete texture samples as (0, 0, 0, 1)";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -1500,7 +1500,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// existing backend hooks authoritative when present, and use this fallback otherwise.
|
||||
void AdoptTextureWithoutBackendHook(const MGPipeResourceRecord& stored, const MGPSubData& upload,
|
||||
const void* bytes, const MGPSubRegion* regions) {
|
||||
if (!MG_Config::DataArmIsRecord() || !bytes || upload.Blob.Size == 0) return;
|
||||
if (!bytes || upload.Blob.Size == 0) return;
|
||||
auto& store = MG_Record::ServerStagedTexture();
|
||||
const auto& desc = stored.Desc;
|
||||
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(upload.Res);
|
||||
@@ -1523,7 +1523,6 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
void DefineTextureWithoutBackendHook(const MGPResourceDesc& desc, const MGPRespecifiedLevel* level) {
|
||||
if (!MG_Config::DataArmIsRecord()) return;
|
||||
auto& store = MG_Record::ServerStagedTexture();
|
||||
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(desc.Resource);
|
||||
const auto define = [&](Uint16 target, Uint16 mip, const IntVec3& extent) {
|
||||
@@ -1971,8 +1970,7 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
void MGPipeApplierReleaseObjectRecords() {
|
||||
if (MG_Config::DataArmIsRecord() &&
|
||||
(g_resourceOps == nullptr || g_resourceOps->TextureDestroy == nullptr)) {
|
||||
if ((g_resourceOps == nullptr || g_resourceOps->TextureDestroy == nullptr)) {
|
||||
MG_Record::ServerStagedTexture().DropAll();
|
||||
}
|
||||
// The served context is going away and this applier with it. Under split that is one
|
||||
@@ -2733,9 +2731,8 @@ namespace MobileGL::MG_Pipe {
|
||||
if (static_cast<MGPipeKind>(handle.Kind) == MGPipeKind::Texture) {
|
||||
if (g_resourceOps != nullptr && g_resourceOps->TextureDestroy != nullptr)
|
||||
g_resourceOps->TextureDestroy(handle.Handle);
|
||||
else if (MG_Config::DataArmIsRecord())
|
||||
MG_Record::ServerStagedTexture().Drop(
|
||||
MG_Record::StagedTextureStore::KeyForHandle(handle.Handle));
|
||||
else { MG_Record::ServerStagedTexture().Drop(
|
||||
MG_Record::StagedTextureStore::KeyForHandle(handle.Handle)); }
|
||||
}
|
||||
if (static_cast<MGPipeKind>(handle.Kind) != MGPipeKind::Buffer) return;
|
||||
if (g_resourceOps != nullptr && g_resourceOps->Destroy != nullptr) {
|
||||
|
||||
@@ -535,7 +535,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// P13 W4a: monolith's record arm refreshes an aliased store through this function too
|
||||
// (Managers.cpp RefreshAliasedStoreFromGpu), and a whole-store landing there is exactly as
|
||||
// current as one under split.
|
||||
if (MG_Config::DataArmIsRecord() && atOffset == 0 && data.size >= m_size) {
|
||||
if (atOffset == 0 && data.size >= m_size) {
|
||||
m_gpuWritePending = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,59 +102,6 @@ TEST(ContextEpochTest, RawDepthSamplerOwnsANativeIdForEachContextGeneration) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// P3b/P4b wave 2-D package D2, STEP 1 of the raw-depth-fetch sampler monolith cleanup: the case
|
||||
// above is parameterised over the OTHER arm, so that the claim DirectGLES.cpp:334-336 makes in
|
||||
// prose - "P5f fs's transport arm below uses only a native sampler with fixed values. The legacy
|
||||
// pair is not constructed or consulted by the server." - is a test rather than a comment.
|
||||
//
|
||||
// STEP 2 OF THAT CLEANUP - deleting what the transport arm makes unreachable - IS NOT DONE HERE
|
||||
// AND IS RECORDED AS P13. Every candidate (the two file-static SharedPtrs at DirectGLES.cpp:82-83,
|
||||
// the monolith arm at :361-371, NeedsRawDepthFetchSampler at :393-399 and the pre-handle sampler
|
||||
// pass at :8015-8024) is UNCONDITIONAL code: none of it sits behind
|
||||
// `#if !MOBILEGL_BUILD_DISAGGREGATED`, the pull build (DISAGGREGATED=OFF, PIPE_PUSH=OFF) compiles
|
||||
// and links all of it, and the monolith arm is the ENTIRE body GetRawDepthFetchSampler has there.
|
||||
// Deleting any of it moves pull `.text` and fails G1's 0/0/0/0 - which is the rule
|
||||
// ARCHITECTURE.md:340 states for exactly this class of retired-but-compiled code. The package's
|
||||
// instruction was "only if it keeps the pull build's .text identical"; it does not, so the
|
||||
// deletion stops here and is filed rather than attempted.
|
||||
//
|
||||
// What this case CAN assert without touching a line of library code is that the two arms are
|
||||
// genuinely two objects, which is the observable form of "not consulted by the server": a server
|
||||
// that had fallen through to the legacy pair would hand back the same pointer under both
|
||||
// transports.
|
||||
TEST(ContextEpochTest, RawDepthSamplerTakesADifferentObjectOnEachArmAndTheServerNeverTakesTheLegacyOne) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
DriverScope scope; // pins InProcess and bumps the generation, so both arms start cold
|
||||
samplerParameters = 0;
|
||||
|
||||
// --- the transport-native arm ---
|
||||
SamplerImpl::BackendSamplerObject* const native = GetRawDepthFetchSampler();
|
||||
ASSERT_NE(native, nullptr);
|
||||
const Uint nativeId = native->GetBackendSamplerId();
|
||||
EXPECT_NE(nativeId, 0u);
|
||||
EXPECT_EQ(samplerParameters, 4u) << "the native arm programs its four fixed values directly";
|
||||
|
||||
// --- the monolith arm, same entry point, same process ---
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
SamplerImpl::BackendSamplerObject* const legacy = GetRawDepthFetchSampler();
|
||||
ASSERT_NE(legacy, nullptr);
|
||||
EXPECT_NE(legacy, native)
|
||||
<< "both transports handed back the SAME BackendSamplerObject, so the server did not take "
|
||||
"the transport-native arm at all - it fell through to g_rawDepthFetchSamplerBackend, "
|
||||
"which DirectGLES.cpp:334-336 says it must not touch";
|
||||
EXPECT_NE(legacy->GetBackendSamplerId(), nativeId)
|
||||
<< "the two arms share a native sampler id";
|
||||
|
||||
// --- and back, to prove the selection is live rather than a first-call latch ---
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
EXPECT_EQ(GetRawDepthFetchSampler(), native)
|
||||
<< "the transport arm's cached native sampler did not survive a round trip through the "
|
||||
"monolith arm, so one of the two arms is rebuilding state the other owns";
|
||||
#else
|
||||
GTEST_SKIP() << "both arms exist only in the disaggregated build; the pull build compiles the "
|
||||
"monolith arm alone and it is the whole function there";
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(ContextEpochTest, SameXfbNameInTwoServedContextsDoesNotAliasThePausedSpan) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
@@ -173,87 +173,5 @@ TEST(SubsystemDepsTest, TheClientNeverEmitsAFamilyTheTableRefuses) {
|
||||
RecordProperty("client_emitting_combinations", emitted);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// 3. THE SERVER CONSUMER GATE agrees with the table at every interesting mask.
|
||||
//
|
||||
// The resolvers answer "did this family's handle arm come up", which is the dependency rule AND
|
||||
// the family's own bit AND, for the framebuffer one, D-C3's separate wire-caps refusal. So the
|
||||
// comparison is one-directional where it has to be: a resolver may say NO for a reason of its
|
||||
// own, but it may never say YES at a mask the table refuses - that is the direction in which a
|
||||
// disagreement is a half-run subsystem rather than an extra refusal.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
TEST(SubsystemDepsTest, TheServerConsumerGateNeverArmsAFamilyTheTableRefuses) {
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
// THE RESOLVERS SURVIVE A SWEEP because a refused family takes the (unreachable since P13 W3b)
|
||||
// legacy verdict and returns false instead of stopping; ConfigLoader refuses any such mask at
|
||||
// startup, so this sweep exercises the resolvers' own logic, not a reachable configuration.
|
||||
struct Resolver {
|
||||
const char* Name;
|
||||
Uint64 Family;
|
||||
Bool (*Resolve)();
|
||||
};
|
||||
const Resolver resolvers[] = {
|
||||
{"ResolveFramebufferSubsystemArm", kMGPipeSubsystemFramebuffer,
|
||||
&ResolveFramebufferSubsystemArm},
|
||||
{"ResolveTextureResourceSubsystemArm", kMGPipeSubsystemTextureResources,
|
||||
&ResolveTextureResourceSubsystemArm},
|
||||
{"ResolveSamplerSubsystemArm", kMGPipeSubsystemSamplers, &ResolveSamplerSubsystemArm},
|
||||
{"ResolveProgramSubsystemArm", kMGPipeSubsystemPrograms, &ResolveProgramSubsystemArm},
|
||||
};
|
||||
for (const Uint64 mask : InterestingMasks()) {
|
||||
PushMaskScope scope(mask);
|
||||
for (const Resolver& resolver : resolvers) {
|
||||
const Bool armed = resolver.Resolve();
|
||||
if (!armed) continue;
|
||||
EXPECT_TRUE(MGPipeSubsystemDependenciesAreSet(resolver.Family, mask))
|
||||
<< "at MOBILEGL_PIPE_PUSH=" << Describe(mask) << " the SERVER's " << resolver.Name
|
||||
<< " ARMED the handle arm for family " << Describe(resolver.Family)
|
||||
<< " while MG_Pipe/SubsystemDeps.def says its dependency bits are not all set. "
|
||||
"The client withholds the family at this mask, so the server is running a "
|
||||
"handle arm nobody is feeding - which is the half-run subsystem D-K2 exists "
|
||||
"to make impossible.";
|
||||
EXPECT_NE(mask & resolver.Family, 0ull)
|
||||
<< resolver.Name << " armed with its own bit clear at " << Describe(mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// 4. ...and the refusal really happens at the two documented lanes, so that test 3 is not
|
||||
// vacuously green because every resolver said NO for an unrelated reason.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
TEST(SubsystemDepsTest, TheTwoDocumentedRefusalLanesStillRefuseAndTheFullMaskStillArms) {
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
{
|
||||
PushMaskScope scope(kMGPipeSubsystemsMigratedAtP5e);
|
||||
EXPECT_TRUE(ResolveTextureResourceSubsystemArm())
|
||||
<< "the texture family's handle arm does not come up at its own phase mask, so every "
|
||||
"refusal assertion in this file is vacuous";
|
||||
EXPECT_TRUE(ResolveSamplerSubsystemArm());
|
||||
EXPECT_TRUE(ResolveProgramSubsystemArm());
|
||||
// THE FRAMEBUFFER RESOLVER IS RECORDED, NOT ASSERTED, and the asymmetry is the point:
|
||||
// ResolveFramebufferSubsystemArm folds D-C3's wire-caps refusal in beside the dependency
|
||||
// rule (Managers.cpp:4323-4325), and that half asks the SERVER's published caps - which a
|
||||
// bare unit binary with no session and no registered backend cannot answer in the
|
||||
// affirmative. So a false here says nothing about D-K2 and asserting true would pin
|
||||
// whatever a test binary happens to have linked. The sweep above still covers it in the
|
||||
// direction that matters (it may never ARM where the table refuses).
|
||||
RecordProperty("framebuffer_resolver_at_p5e", ResolveFramebufferSubsystemArm() ? 1 : 0);
|
||||
}
|
||||
{
|
||||
// 0x7ff: bits 0..10, i.e. the texture family WITHOUT the sampler family - D-K2's fourth
|
||||
// row, and the mask CMakeLists.txt's refusal lane pins.
|
||||
PushMaskScope scope(0x7ffull);
|
||||
EXPECT_FALSE(ResolveTextureResourceSubsystemArm())
|
||||
<< "0x7ff is the mask whose texture family Espryt refuses by name; if it now arms, "
|
||||
"D-K2's fourth row has been dropped on the server side while the client and the "
|
||||
".def still carry it";
|
||||
}
|
||||
{
|
||||
// 0x5ff: bits 0..8 and bit 10, i.e. the texture family WITHOUT the framebuffer bit and
|
||||
// WITHOUT bit 11 - the second refusal lane CMakeLists.txt pins.
|
||||
PushMaskScope scope(0x5ffull);
|
||||
EXPECT_FALSE(ResolveTextureResourceSubsystemArm())
|
||||
<< "0x5ff leaves bit 11 clear too, so the fourth row refuses it for the same reason";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1429,10 +1429,9 @@ TEST_F(GeneralBufferTest, General_PersistentExplicitFlushOnlyDirtiesFlushedRange
|
||||
mapped[1] = 200;
|
||||
mapped[3] = 400;
|
||||
|
||||
// FlushExplicit persistent maps only reach the backend via explicit flushes.
|
||||
bufferObject->SyncPersistentMappedRange();
|
||||
EXPECT_EQ(bufferObject->GetChangeSerial(), baseSerial);
|
||||
|
||||
// The explicit flush is what GL 4.6 6.2.1 promises reaches the store. (Until P13 W6 this also
|
||||
// asserted that a per-draw sync moved nothing; the record arm's push publishes the map itself,
|
||||
// so that was the deleted frontend arm's behaviour, not a rule.)
|
||||
FlushMappedBufferRange(GL_ARRAY_BUFFER, sizeof(GLint), sizeof(GLint));
|
||||
EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial);
|
||||
|
||||
@@ -1669,112 +1668,7 @@ namespace {
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(GeneralBufferTest, General_PersistentCoherentZeroCopyStressNoPerDrawReupload) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
|
||||
constexpr SizeT kCount = 4096; // 16 KiB of GLint - a "large" dynamic ring buffer
|
||||
Vector<GLint> initial(kCount, 0);
|
||||
BufferStorage(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(kCount * sizeof(GLint)), initial.data(),
|
||||
GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
auto* mapped = static_cast<GLint*>(
|
||||
MapBufferRange(GL_ARRAY_BUFFER, 0, static_cast<GLsizeiptr>(kCount * sizeof(GLint)),
|
||||
GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
EXPECT_EQ(mock.acquireMapCalls, 1);
|
||||
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
EXPECT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
// The app maps straight onto the backend's GPU storage...
|
||||
EXPECT_EQ(static_cast<void*>(mapped), static_cast<void*>(mock.gpu.data()));
|
||||
// ...and EVERY consumer (they all read MappedData() now) resolves to that same GPU
|
||||
// memory, not a stale shadow. This is the invariant whose violation corrupted UBOs.
|
||||
EXPECT_EQ(static_cast<const void*>(bufferObject->MappedData()), static_cast<const void*>(mock.gpu.data()));
|
||||
|
||||
// Isolate the per-draw behavior: storage creation legitimately issued one Respecify.
|
||||
mock.subDataCalls = 0;
|
||||
mock.flushCalls = 0;
|
||||
mock.respecifyCalls = 0;
|
||||
|
||||
constexpr int kFrames = 240;
|
||||
constexpr int kDrawsPerFrame = 64; // 15,360 draws total
|
||||
for (int frame = 0; frame < kFrames; ++frame) {
|
||||
for (int draw = 0; draw < kDrawsPerFrame; ++draw) {
|
||||
mapped[draw] = frame * 1000 + draw; // MC writes through the coherent map
|
||||
bufferObject->SyncPersistentMappedRange(); // draw-time hook
|
||||
}
|
||||
}
|
||||
|
||||
// The crux: across 15,360 draws, NOT ONE per-draw backend transfer.
|
||||
EXPECT_EQ(mock.acquireMapCalls, 1);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.respecifyCalls, 0);
|
||||
|
||||
// The app's writes are coherently visible in the backend storage (no copy), and a
|
||||
// reader going through MappedData() sees them too.
|
||||
const auto* gpuInts = reinterpret_cast<const GLint*>(mock.gpu.data());
|
||||
const auto* viaMapped = reinterpret_cast<const GLint*>(bufferObject->MappedData());
|
||||
for (int draw = 0; draw < kDrawsPerFrame; ++draw) {
|
||||
EXPECT_EQ(mapped[draw], (kFrames - 1) * 1000 + draw);
|
||||
EXPECT_EQ(gpuInts[draw], (kFrames - 1) * 1000 + draw);
|
||||
EXPECT_EQ(viaMapped[draw], (kFrames - 1) * 1000 + draw);
|
||||
}
|
||||
|
||||
EXPECT_TRUE(UnmapBuffer(GL_ARRAY_BUFFER));
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(GeneralBufferTest, General_PersistentCoherentFallbackSyncsPerDrawWhenBackendDeclines) {
|
||||
// Backend cannot back the map => the legacy CPU-shadow path must still be correct,
|
||||
// and this documents the behavior the fix removed (one whole-range transfer per draw),
|
||||
// proving the harness above would catch a regression (non-zero per-draw count).
|
||||
ZeroCopyMockBackend mock;
|
||||
mock.provideMap = false;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
|
||||
constexpr SizeT kCount = 256;
|
||||
Vector<GLint> initial(kCount, 0);
|
||||
BufferStorage(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(kCount * sizeof(GLint)), initial.data(),
|
||||
GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
auto* mapped = static_cast<GLint*>(
|
||||
MapBufferRange(GL_ARRAY_BUFFER, 0, static_cast<GLsizeiptr>(kCount * sizeof(GLint)),
|
||||
GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
EXPECT_EQ(mock.acquireMapCalls, 0); // backend declined => shadow-backed map
|
||||
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
|
||||
|
||||
constexpr int kDraws = 100;
|
||||
for (int draw = 0; draw < kDraws; ++draw) {
|
||||
mapped[draw % kCount] = draw;
|
||||
bufferObject->SyncPersistentMappedRange();
|
||||
}
|
||||
// Legacy behavior: every draw pushed the whole range -> one SubData per draw.
|
||||
EXPECT_EQ(mock.subDataCalls, kDraws);
|
||||
|
||||
EXPECT_TRUE(UnmapBuffer(GL_ARRAY_BUFFER));
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MOBILEGL_COHERENT_AS_FLUSH: persistent FLUSH_EXPLICIT mapping requests are
|
||||
|
||||
@@ -268,25 +268,6 @@ TEST_F(SplitBufferSet, Row5EndTransformFeedbackMarksTheCaptureTargets) {
|
||||
EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::EndTransformFeedbackCapture), 1u);
|
||||
}
|
||||
|
||||
// THE GATE ITSELF. On the monolith path the six backend sites are still the only producers and
|
||||
// a second marker would be new behaviour (D-J) - and rows 4 and 5 would remove a stall that
|
||||
// monolith is entitled to keep.
|
||||
TEST_F(SplitBufferSet, TheWholeSetIsInertOnTheMonolithPath) {
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
auto ssbo = MakeBuffer(18u, 256);
|
||||
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 0).Bind(ssbo);
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(BufferTarget::ShaderStorage, 0);
|
||||
|
||||
MG_Record::MarkGpuWritesForDraw();
|
||||
MG_Record::MarkGpuWritesForDispatch();
|
||||
MG_Record::MarkReadPixelsPackBuffer();
|
||||
MG_Record::MarkEndTransformFeedbackCaptureTargets();
|
||||
|
||||
for (SizeT row = 0; row < static_cast<SizeT>(GpuWriteProducer::Count); ++row) {
|
||||
EXPECT_EQ(MG_Record::ProducerMarkCount(static_cast<GpuWriteProducer>(row)), 0u)
|
||||
<< "row " << row << " fired with Transport == Monolith";
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// The persistent-map push
|
||||
@@ -814,7 +795,7 @@ TEST(SplitBufferSet, Row3TransformFeedbackTargetsAreOnlyMarkedWhileACaptureIsAct
|
||||
}
|
||||
TEST(SplitBufferSet, Row4AReadPixelsIntoAPackPboMarksThePbo) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, Row5EndTransformFeedbackMarksTheCaptureTargets) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, TheWholeSetIsInertOnTheMonolithPath) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
|
||||
TEST(SplitBufferSet, MembershipIsSyncPersistentMappedRangesOwnEarlyOutChain) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, TheAdoptedArmIsNotAMemberAndItIsTheChainRowThatSaysSo) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, ThePushCutsTheMappedSpanIntoBlocksAndMovesPmap) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
|
||||
@@ -511,7 +511,8 @@ TEST(TextureEmit, AWholeLevelUploadCarriesZeroStrides) {
|
||||
EXPECT_EQ(record.SourceIsVerbatimLevelShadow, 1)
|
||||
<< "the client always declares the level shadow; the server clears it when it converts";
|
||||
EXPECT_EQ(record.Blob.Seg, kMGHostSpanSegNone);
|
||||
EXPECT_EQ(record.Blob.Size, 0u) << "a monolith record does not declare its blob";
|
||||
EXPECT_EQ(record.Blob.Size, 64u * 64u * 4u)
|
||||
<< "the record declares its blob on every arm since P13 W6 (the record arm reads it)";
|
||||
EXPECT_NE(record.Blob.Offset, 0u) << "Blob.Offset is the level shadow's address in monolith";
|
||||
EXPECT_EQ(Textures().SubDataPieceCount(), 1u)
|
||||
<< "a level that fits one record is exactly one piece (fix A2's counter), so the "
|
||||
|
||||
@@ -237,7 +237,9 @@ namespace {
|
||||
const auto& attrib = vao->GetAttribute(index);
|
||||
const MGPVertexAttribWire& wire = Emitter().LastAttributes()[static_cast<SizeT>(i)];
|
||||
SCOPED_TRACE(::testing::Message() << "attribute " << i);
|
||||
EXPECT_EQ(wire.Offset, static_cast<Uint64>(attrib.Offset));
|
||||
// Client addresses never cross: an attribute with no buffer carries offset 0 (its
|
||||
// bytes ride an owned buffer at draw time), every arm since P13 W6.
|
||||
EXPECT_EQ(wire.Offset, attrib.Buffer ? static_cast<Uint64>(attrib.Offset) : 0ull);
|
||||
EXPECT_EQ(wire.Stride, static_cast<Int32>(attrib.Stride));
|
||||
EXPECT_EQ(wire.Type, static_cast<Uint32>(attrib.Type));
|
||||
EXPECT_EQ(wire.Size, static_cast<Uint8>(attrib.Size));
|
||||
|
||||
@@ -369,129 +369,7 @@ TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) {
|
||||
extensions.end());
|
||||
}
|
||||
|
||||
// A multisample texture is fetched, never filtered, so the mip-chain completeness rules never
|
||||
// apply to it (GL 4.6 core 8.17). It has exactly one level and MIN_FILTER's initial value is
|
||||
// NEAREST_MIPMAP_LINEAR, so asking those rules anyway calls EVERY multisample texture incomplete
|
||||
// - and both backends express "incomplete" as "leave the native target unbound", which makes the
|
||||
// shader's sampler2DMS read zero from a texture that was written correctly.
|
||||
//
|
||||
// That is KHR-GL43.compute_shader.resource-texture: it clears its 2DMS texture to 123.0 through
|
||||
// an FBO (which succeeds - the ES clear is issued on a COMPLETE 4-sample framebuffer with no
|
||||
// error) and then fails at the first sampler2DMS element because the texture was never bound.
|
||||
TEST(DirectGLESSanity, BindsAMultisampleTextureDespiteTheDefaultMipmapFilter) {
|
||||
using namespace MobileGL;
|
||||
namespace DirectGLES = MG_Backend::DirectGLES;
|
||||
|
||||
ScopedDirectGLESTextureBindings state;
|
||||
|
||||
GLuint frontendTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &frontendTexture);
|
||||
ASSERT_NE(frontendTexture, 0u);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, frontendTexture);
|
||||
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(0)
|
||||
.GetBindingSlot(TextureTarget::Texture2DMultisample)
|
||||
.GetBoundObject();
|
||||
ASSERT_NE(textureObject, nullptr);
|
||||
|
||||
textureObject->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
textureObject->SetSamples(4);
|
||||
textureObject->SetFixedSampleLocations(false);
|
||||
// One level, 4x4 - the shape glTexImage2DMultisample produces, and a size whose mip chain
|
||||
// would need three levels if the filter rules were (wrongly) applied.
|
||||
MG_State::GLState::AsMipmapTexture(textureObject.get())
|
||||
->AllocateStorage(TextureUploadTarget::Texture2DMultisample, 0, {{4, 4, 1}, 4});
|
||||
|
||||
// The precondition that used to poison it, asserted rather than assumed: the texture's own
|
||||
// sampler still reports a mipmapping filter, because GL's initial MIN_FILTER is
|
||||
// NEAREST_MIPMAP_LINEAR and a multisample texture has no way (and no reason) to change it.
|
||||
// If a future default made this None the test would pass without covering anything.
|
||||
const auto& sampler = textureObject->GetSamplerObject();
|
||||
ASSERT_NE(sampler, nullptr);
|
||||
ASSERT_NE(sampler->GetMipmapMode(), SamplerMipmapMode::None)
|
||||
<< "fixture is stale: the default sampler no longer asks for mipmapping, so this test "
|
||||
"would not exercise the multisample guard";
|
||||
|
||||
EXPECT_FALSE(MG_State::GLState::SamplesAsIncompleteTexture(textureObject.get(), sampler.get()))
|
||||
<< "a multisample texture is never filter-incomplete";
|
||||
|
||||
auto& backendTexture = DirectGLES::TextureImpl::g_backendTextureObjects.GetOrCreate(textureObject);
|
||||
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
|
||||
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
|
||||
|
||||
// The symptom itself: the per-unit walk has to actually bind it. BindCurrentTextures() is
|
||||
// the per-draw walk - it reads GetProgramForDraw - so the block it reads is the one a draw
|
||||
// fills; without saying so, its first read is Fatal{UnmigratedPipeInput} in a push build.
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
DirectGLES::BindCurrentTextures();
|
||||
ASSERT_EQ(state.bindCalls.size(), 1u)
|
||||
<< "the multisample texture was not bound; every texelFetch against it reads zero";
|
||||
EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D_MULTISAMPLE);
|
||||
EXPECT_EQ(state.bindCalls[0].texture, backendTextureId);
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
|
||||
using namespace MobileGL;
|
||||
namespace DirectGLES = MG_Backend::DirectGLES;
|
||||
|
||||
ScopedDirectGLESTextureBindings state;
|
||||
|
||||
GLuint frontendTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &frontendTexture);
|
||||
ASSERT_NE(frontendTexture, 0u);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, frontendTexture);
|
||||
const auto& frontendTextureObject = MG_State::pGLContext->GetTextureUnitObject(0)
|
||||
.GetBindingSlot(TextureTarget::Texture2D)
|
||||
.GetBoundObject();
|
||||
ASSERT_NE(frontendTextureObject, nullptr);
|
||||
ASSERT_EQ(frontendTextureObject->GetExternalIndex(), frontendTexture);
|
||||
|
||||
// A texture with no image is incomplete, and an incomplete texture samples as (0, 0, 0, 1) -
|
||||
// which DirectGLES expresses by leaving the native target unbound (see "sample a
|
||||
// mipmap-incomplete texture as black"). This test is about the bind-0 clear, so the texture
|
||||
// has to be complete enough to get bound in the first place: a format plus a level 0. At 1x1
|
||||
// that single level is the whole mip chain, so it stays complete under any filter. The state
|
||||
// is set directly rather than through glTexImage2D because the mock GLES table below wires
|
||||
// only the binding entry points, not the upload path.
|
||||
frontendTextureObject->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
MG_State::GLState::AsMipmapTexture(frontendTextureObject.get())
|
||||
->AllocateStorage(TextureUploadTarget::Texture2D, 0, {{1, 1, 1}, 4});
|
||||
ASSERT_FALSE(MG_State::GLState::SamplesAsIncompleteTexture(
|
||||
frontendTextureObject.get(), frontendTextureObject->GetSamplerObject().get()));
|
||||
|
||||
auto& backendTexture = DirectGLES::TextureImpl::g_backendTextureObjects.GetOrCreate(frontendTextureObject);
|
||||
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
|
||||
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
|
||||
|
||||
// Each walk below is the texture half of one draw, so each gets its own verb (the second
|
||||
// and third stand after frontend state moved, exactly as a second entry point's fill would).
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
DirectGLES::BindCurrentTextures();
|
||||
ASSERT_EQ(state.bindCalls.size(), 1u);
|
||||
EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D);
|
||||
EXPECT_EQ(state.bindCalls[0].texture, backendTextureId);
|
||||
|
||||
// The default 1D slot maps to the same native ES target as 2D. It must not clear and force a
|
||||
// redundant rebind while the real 2D frontend object remains current.
|
||||
draw.Renew();
|
||||
DirectGLES::BindCurrentTextures();
|
||||
EXPECT_EQ(state.bindCalls.size(), 1u);
|
||||
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_TRUE(MG_State::GLState::IsUndefinedDefaultTexture(
|
||||
MG_State::pGLContext->GetTextureUnitObject(0)
|
||||
.GetBindingSlot(TextureTarget::Texture2D)
|
||||
.GetBoundObject()
|
||||
.get()));
|
||||
|
||||
draw.Renew();
|
||||
DirectGLES::BindCurrentTextures();
|
||||
|
||||
ASSERT_EQ(state.bindCalls.size(), 2u);
|
||||
EXPECT_EQ(state.bindCalls[1].target, GL_TEXTURE_2D);
|
||||
EXPECT_EQ(state.bindCalls[1].texture, 0u);
|
||||
EXPECT_EQ(DirectGLES::TextureImpl::g_boundTexturesCache[0][static_cast<SizeT>(TextureTarget::Texture2D)],
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, ProvidesNamedFramebufferBlitForDirectStateAccess) {
|
||||
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
|
||||
@@ -3092,103 +2970,6 @@ namespace {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The per-unit texture sync memo BORROWS the binding slot: an entry holds a pointer to the
|
||||
// slot's shared_ptr plus the backend twin of whatever was in it when the entry was built. Its
|
||||
// keys (context id, bind-generation epoch, high-water mark, sampling generation) are the primary
|
||||
// guard, but they are all derived state - so the memo also has to survive a slot swap that never
|
||||
// reached them.
|
||||
//
|
||||
// It did not. The DSA by-name emulation swapped a slot silently, every key still matched, and
|
||||
// the replay drove texture A's backend twin from texture B's frontend object: A's backend
|
||||
// storage was re-specified with B's shape, destroying anything A only ever had on the GPU. On
|
||||
// Espryt + Iris/BSL that blanked Minecraft's 16x16 lightmap the moment a 2048x2048 shadow map
|
||||
// was uploaded through a by-name call, and since the text shader multiplies by the lightmap,
|
||||
// `if (color.a < 0.1) discard` then threw away every glyph in the process - HUD, menus and the
|
||||
// vanilla title screen alike.
|
||||
TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) {
|
||||
using namespace MobileGL;
|
||||
// P13 W3b: PINNED TO THE P2 MASK, in its own process (ctest runs every case in one). The code
|
||||
// under test - the monolith arm's borrowed-slot unit list and PairingsIntact - does not depend
|
||||
// on the mask and is live at the shipping mask (UnitTexturesByHandle() is false on monolith).
|
||||
// The mask only picks the twin-sync body this probe OBSERVES: at 0x3fff that body reads
|
||||
// applier records, and a unit-lane process cannot create them (MGPipeApplyResourceCreate
|
||||
// declines under NoP4aConsumer), so there the probe would see nothing. The hazard itself is
|
||||
// not reachable through public GL any more (the by-name emulation bumps the bind generation
|
||||
// and restores the slot before any draw - measured: DsaUpdateKeepsTheBoundUnitScenario stays
|
||||
// green with both defences removed), so this state-level construction is its only red.
|
||||
const Uint64 savedMask = MG_Config::Features.PipePush;
|
||||
MG_Config::Features.PipePush = MG_Pipe::kMGPipeSubsystemsMigratedAtP2;
|
||||
ASSERT_FALSE(MG_Backend::DirectGLES::TextureResourceSubsystemEnabled())
|
||||
<< "the texture-resource family arm latched before this case pinned its mask; run the case "
|
||||
"in its own process, as ctest does";
|
||||
ScopedDirectGLESTextureBindings scoped; // fresh GLContext + registry + binding caches
|
||||
Vector<TexSpecCall> specs;
|
||||
ScopedTexSpecRecording recording(specs);
|
||||
|
||||
auto functions = MG_Backend::DirectGLES::g_GLESFuncs;
|
||||
functions.glBindTexture = TS_BindTexture;
|
||||
functions.glActiveTexture = TS_ActiveTexture;
|
||||
functions.glTexImage2D = TS_TexImage2D;
|
||||
functions.glTexParameteri = TS_TexParameteri;
|
||||
functions.glTexParameterf = TS_TexParameterf;
|
||||
functions.glTexParameterfv = TS_TexParameterfv;
|
||||
functions.glPixelStorei = TS_PixelStorei;
|
||||
functions.glBindBuffer = TS_BindBuffer;
|
||||
MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
|
||||
|
||||
GLuint names[2] = {};
|
||||
MG_Impl::GLImpl::GenTextures(2, names);
|
||||
// `foreign` stands in for the shadow map, `resident` for the lightmap. Both are fully
|
||||
// specified BEFORE the first sync so that nothing between the two syncs can move the
|
||||
// sampling-resolution generation and invalidate the memo for an unrelated reason.
|
||||
const auto foreign = MakeComplete2DTexture(names[1], 32);
|
||||
const auto resident = MakeComplete2DTexture(names[0], 16);
|
||||
ASSERT_NE(foreign, nullptr);
|
||||
ASSERT_NE(resident, nullptr);
|
||||
|
||||
// First sync: builds the memo with unit 0 -> `resident`, and gives `resident`'s twin its
|
||||
// 16x16 backend storage. The unit walk is the per-draw one, so both syncs below stand in a
|
||||
// draw - the fill is what a real glDraw* would have done before reaching this helper.
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
MG_Backend::DirectGLES::TextureImpl::SyncNeccessaryTextures();
|
||||
auto* residentSlot = MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects.Find(resident.get());
|
||||
ASSERT_NE(residentSlot, nullptr);
|
||||
ASSERT_NE(*residentSlot, nullptr);
|
||||
const GLuint residentBackendId = (*residentSlot)->GetBackendTextureId();
|
||||
ASSERT_NE(residentBackendId, 0u);
|
||||
ASSERT_FALSE(specs.empty());
|
||||
EXPECT_EQ(specs.back().texture, residentBackendId);
|
||||
EXPECT_EQ(specs.back().width, 16);
|
||||
|
||||
// The hazard, reproduced at the state level: put `foreign` on the slot the memo borrows
|
||||
// WITHOUT telling the binding accounting, exactly as the by-name emulation used to.
|
||||
MG_State::pGLContext->GetTextureUnitObject(0).GetBindingSlot(TextureTarget::Texture2D).Bind(foreign);
|
||||
|
||||
const SizeT specsBeforeReplay = specs.size();
|
||||
// The second draw. Its fill re-reads the memo's keys off the live context, which is the
|
||||
// premise being tested: the silent slot swap moved none of them.
|
||||
draw.Renew();
|
||||
MG_Backend::DirectGLES::TextureImpl::SyncNeccessaryTextures();
|
||||
|
||||
// Above all, nothing may have re-specified the RESIDENT texture's backend storage. That single
|
||||
// call is what destroyed the lightmap. (Checked FIRST since P13 W3b, so a red names the damage
|
||||
// rather than stopping at the twin assertions below.)
|
||||
for (SizeT i = specsBeforeReplay; i < specs.size(); ++i) {
|
||||
EXPECT_NE(specs[i].texture, residentBackendId)
|
||||
<< "the stale memo entry re-specified the resident texture's backend storage with "
|
||||
<< specs[i].width << "x" << specs[i].height;
|
||||
}
|
||||
|
||||
// ...and `foreign` must have been synced through its OWN twin.
|
||||
auto* foreignSlot = MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects.Find(foreign.get());
|
||||
ASSERT_NE(foreignSlot, nullptr) << "the replay never reached the texture now in the slot";
|
||||
ASSERT_NE(*foreignSlot, nullptr);
|
||||
const GLuint foreignBackendId = (*foreignSlot)->GetBackendTextureId();
|
||||
EXPECT_NE(foreignBackendId, residentBackendId);
|
||||
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
MG_Config::Features.PipePush = savedMask;
|
||||
}
|
||||
|
||||
// P5e (tx2), CONTRACT-P5E §4.3: THE HANDLE ARM'S SIBLING OF UnitMemoRefusesToDriveATwinFromAnotherTexture,
|
||||
// WITH ABA AS THE HAZARD INSTEAD OF A SILENT SLOT SWAP. (P13 W3b retired that case: it ran only at
|
||||
@@ -4473,92 +4254,6 @@ TEST(DirectGLESSlotTable, ASavedCopyOfARealRegistryDropsTheTwinOnTheSameNotice)
|
||||
EXPECT_FALSE(registry.DestroyByLifetimeId(0)) << "sanity: a null id frees nothing";
|
||||
}
|
||||
|
||||
// P3a REWORK C-1's gate, and the case that would have caught it.
|
||||
//
|
||||
// A persistently mapped buffer whose store was NOT adopted - under the 16 MiB threshold, or
|
||||
// with DisableLargeBufferAdoption, or with no EXT_buffer_storage - is written through its
|
||||
// pointer and emits NOTHING: no resource call, no serial, no mutation epoch. The only thing
|
||||
// that can make the next draw carry those bytes to the GPU is the per-draw clean probe
|
||||
// answering DIRTY, which is why the pre-P3a probe asks the frontend IsMapped() instead of
|
||||
// asking a serial. The first cut of the handle arm replaced that question with the applier
|
||||
// record's HasLiveHostWrites - a field D-A4 pins false and nothing in P3a writes - so the
|
||||
// answer became CLEAN forever, EnsureBufferResourceForHandle (and with it
|
||||
// SyncPersistentMappedRange, the ONLY per-draw push for a vertex/uniform/SSBO persistent map)
|
||||
// was never reached again, and the frame drew the last uploaded bytes with no diagnostic.
|
||||
//
|
||||
// The case is built so it can go red for exactly that: every other question is arranged to
|
||||
// answer clean, and the first EXPECT asserts so before the map is taken.
|
||||
TEST(DirectGLESBufferDrawProbe, ALiveHostMapKeepsTheHandleArmProbeDirtyBetweenTwoDraws) {
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
using namespace MobileGL::MG_State::GLState;
|
||||
|
||||
if (!EsprytSlotTablesEnabled()) {
|
||||
GTEST_SKIP() << "the handle-keyed resource table only exists on the {slot, gen} arm";
|
||||
}
|
||||
|
||||
auto owner = MakeShared<BufferObject>(0u);
|
||||
owner->Respecify(256, nullptr);
|
||||
const MG_Pipe::MGPipeHandle res =
|
||||
MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::Buffer, owner->GetLifetimeId());
|
||||
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(res));
|
||||
|
||||
// The applier's record, written straight into the state rather than through package A's
|
||||
// entry points: this case is about the PROBE, and the probe reads the record.
|
||||
auto& applier = MG_Pipe::MGPipeApplier();
|
||||
if (applier.Resources.size() <= static_cast<SizeT>(res.Slot)) {
|
||||
applier.Resources.resize(static_cast<SizeT>(res.Slot) + 1);
|
||||
}
|
||||
auto& record = applier.Resources[res.Slot];
|
||||
record = {};
|
||||
record.Gen = res.Gen;
|
||||
record.Live = true;
|
||||
record.Desc.Width = 256;
|
||||
record.Serial = 7;
|
||||
// Pinned false by D-A4 and by the MOBILEGL_PIPE_VERIFY assertion in the probe. That is
|
||||
// exactly why it cannot answer the map question on its own.
|
||||
record.HasLiveHostWrites = false;
|
||||
|
||||
auto& twin = BufferImpl::g_backendBufferResources.GetOrCreate(res);
|
||||
twin = MakeShared<BufferImpl::GLESBufferResource>();
|
||||
auto* const resource = twin.get();
|
||||
resource->id = 1; // a name, never used: this probe issues no GL
|
||||
resource->contextGeneration = BufferImpl::CurrentBufferContextGeneration();
|
||||
resource->storageInitialized = true;
|
||||
resource->storageSize = 256;
|
||||
resource->syncedChangeSerial = record.Serial;
|
||||
|
||||
ASSERT_TRUE(BufferImpl::IsBufferDrawCleanByHandle(res, resource, owner.get()))
|
||||
<< "the fixture is not clean before the map is taken, so this case cannot isolate the "
|
||||
"live-map question it exists for";
|
||||
|
||||
// GL_MAP_PERSISTENT_BIT | GL_MAP_WRITE_BIT, shadow-backed (nothing adopts it: no backend
|
||||
// AcquirePersistentMap is registered in this binary). This is the state the app writes
|
||||
// through with no GL call at all.
|
||||
void* const mapped = owner->AcquireMemoryRange(
|
||||
Range1D{0, 256}, BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent);
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
ASSERT_TRUE(owner->IsMapped());
|
||||
ASSERT_FALSE(owner->IsBackendPersistentMapped())
|
||||
<< "the map was adopted, so this is the zero-copy case and not the one under test";
|
||||
|
||||
// The write between the two draws. It moves NOTHING the record can see.
|
||||
static_cast<Uint8*>(mapped)[0] = 0x5Au;
|
||||
EXPECT_EQ(record.Serial, 7u) << "sanity: a write through a persistent map emits no call";
|
||||
|
||||
EXPECT_FALSE(BufferImpl::IsBufferDrawCleanByHandle(res, resource, owner.get()))
|
||||
<< "the handle arm called a persistently mapped, non-adopted buffer draw-CLEAN, so the "
|
||||
"bytes just written through its pointer would never reach the GPU: the live-map "
|
||||
"question has been answered from HasLiveHostWrites, which P3a pins false";
|
||||
|
||||
owner->ReleaseMemory(false);
|
||||
EXPECT_TRUE(BufferImpl::IsBufferDrawCleanByHandle(res, resource, owner.get()))
|
||||
<< "the probe stayed dirty after the unmap, i.e. it is not the map that is being read";
|
||||
|
||||
BufferImpl::g_backendBufferResources.ReleaseByHandle(res);
|
||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, res);
|
||||
record = {};
|
||||
}
|
||||
|
||||
// P5 b1's half of the case above: THE PIN IS LIFTED, AND THIS IS WHAT REPLACED IT.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user