mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Merge] (P8): integrate package C - the Espryt split server refreshes its staged copy of a buffer a shader wrote before a CPU reader consumes it (storage/atomic/buffer-image marks, XFB captures into the copy; restart rewrite, multi-draw rebase, *IndirectCount, CPU-fed indirect fields, XFB scatter and fp64 narrowing read through SplitHostBytesForCpuRead); GpuWrittenDrawInputScenario (12 cases) on the Espryt split arms
This commit is contained in:
@@ -483,11 +483,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
|
||||
const Uint8* const base = resource ? BufferImpl::SplitHostBytes(*resource) : nullptr;
|
||||
const Uint8* base = resource ? BufferImpl::SplitHostBytes(*resource) : nullptr;
|
||||
// P8-C: a store nothing ever staged but a shader wrote (the GPU-culling shape)
|
||||
// takes its first copy from this server's own GL buffer. A store that HAS a copy
|
||||
// is not refreshed here: most indirect draws execute natively and read no field
|
||||
// on the CPU, so ExecuteIndexed/ArraysIndirectCommands refresh the fields only
|
||||
// when they consume them.
|
||||
if (base == nullptr && resource != nullptr) {
|
||||
base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_bytes");
|
||||
}
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
if (base == nullptr) {
|
||||
MGLOG_E_ONCE("%s skipped: the verb's indirect buffer {%u, %u} has no staged "
|
||||
"shadow on this side (GPU-written indirect commands are P8's)",
|
||||
"shadow on this side and no GPU write to take one from",
|
||||
label, handle.Slot, handle.Gen);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -769,6 +777,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
entry.Res.Gen);
|
||||
continue;
|
||||
}
|
||||
// P8-C: the SERVER's half of the mark MarkShaderStorageBuffersGpuWritten no longer
|
||||
// makes under a transport - on this side's twin, for this side's CPU readers
|
||||
// (SplitHostBytesForCpuRead). Every bound storage point, which is the client's
|
||||
// own conservative set (ShaderBufferEmit.h marks every one writable); a uniform
|
||||
// point is read-only by definition.
|
||||
if (shaderBufferClass != MG_Pipe::kMGPipeShaderBufferClassUniform) {
|
||||
backendResource->serverGpuWritten = true;
|
||||
}
|
||||
const auto backendBufferId = backendResource->id;
|
||||
if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) {
|
||||
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), backendBufferId);
|
||||
@@ -997,6 +1013,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// from MarkGpuWritesForDraw, WIDER on purpose - it marks every touched
|
||||
// counter point rather than only the ones this program declares, which is
|
||||
// the safe direction for an over-approximate set.
|
||||
//
|
||||
// P8-C: the server's own twin is marked instead, for the server's own CPU
|
||||
// readers (SplitHostBytesForCpuRead) - a counter buffer later bound as a
|
||||
// parameter or element buffer is read on this side.
|
||||
backendResource->serverGpuWritten = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1803,7 +1824,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
const MG_Pipe::MGPipeHandle res = target.handle;
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
|
||||
if (resource == nullptr || resource->persistentMapped) continue;
|
||||
if (resource == nullptr) continue;
|
||||
if (resource->persistentMapped) {
|
||||
// P8-C: nothing to copy - the capture reaches a coherent map -
|
||||
// but this side's CPU readers must finish before reading it.
|
||||
resource->serverGpuWritten = true;
|
||||
continue;
|
||||
}
|
||||
const SizeT size = target.end - target.start;
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId);
|
||||
void* mapped = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget,
|
||||
@@ -1844,6 +1871,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"captured bytes of buffer {%u,%u} cannot reach the client shadow",
|
||||
res.Slot, res.Gen);
|
||||
}
|
||||
// P8-C: the server's own staged copy takes the same bytes, from the
|
||||
// same mapping, so this side's CPU readers see the capture too.
|
||||
BufferImpl::StageServerWrittenRange(*resource, res, mapped, target.start, size);
|
||||
g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget);
|
||||
// Ops_H_Readback's order: writeback, unmap, THEN the serial
|
||||
// stamp, so the next draw does not re-upload the server's stale
|
||||
@@ -2004,7 +2034,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
splitRes.Slot, splitRes.Gen);
|
||||
continue;
|
||||
}
|
||||
const Uint8* hostBytes = splitResource->hostBytes;
|
||||
// P8-C: made current first - a store a shader wrote here is refreshed.
|
||||
const Uint8* hostBytes = BufferImpl::SplitHostBytesForCpuRead(*splitResource, splitRes,
|
||||
"xfb_scatter_pre_capture");
|
||||
// A NULL SHADOW IS NOT A LOST CAPTURE. It is the ORPHANED store
|
||||
// (glBufferData(size, NULL) with no resource_subdata since), which
|
||||
// M-3 declares legal: every byte the application has not staged is
|
||||
@@ -2069,9 +2101,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"scattered capture of buffer {%u,%u} cannot reach the client shadow",
|
||||
splitRes.Slot, splitRes.Gen);
|
||||
}
|
||||
// Ops_H_Readback's stamp, for its reason: the GL store now holds
|
||||
// bytes the server's staged shadow does not, and the next draw must
|
||||
// not re-upload the stale shadow over the capture.
|
||||
// P8-C: the staged copy takes the composed range too, so this side's CPU
|
||||
// readers see the capture (before P8 it kept the pre-capture bytes).
|
||||
BufferImpl::StageServerWrittenRange(*splitResource, splitRes, staged.data(), target.start,
|
||||
rangeBytes);
|
||||
// Ops_H_Readback's stamp, for its reason: the GL store now holds the
|
||||
// capture, and the next draw must not re-upload an older range over it.
|
||||
splitResource->syncedChangeSerial = BufferImpl::ResourceSerialForHandle(splitRes);
|
||||
} else
|
||||
#endif
|
||||
@@ -4425,7 +4460,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// reach of an apply thread entirely.
|
||||
void MarkWritableImageBufferTexturesGpuWritten() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
// P8-C: the SERVER's twin of the buffer behind each writable buffer image, for
|
||||
// this side's CPU readers (SplitHostBytesForCpuRead). Every read is a record:
|
||||
// the unit set SyncImageTextureBinding(view) tracked, the applier's image view,
|
||||
// and the texture descriptor's backing-store handle.
|
||||
if (g_writableImageBufferUnitCount == 0) return;
|
||||
const auto& st = MG_Pipe::MGPipeApplier();
|
||||
for (Uint unit = 0; unit < g_writableImageBufferUnits.size() && unit < st.BoundShaderImages.size();
|
||||
++unit) {
|
||||
if (!g_writableImageBufferUnits[unit]) continue;
|
||||
const MG_Pipe::MGPImageView& view = st.BoundShaderImages[unit];
|
||||
if (!IsWritableImageBufferView(view)) continue;
|
||||
const auto* record = PipeTextureRecordForHandle(view.Res);
|
||||
if (record == nullptr) continue;
|
||||
if (auto* buffer = BufferImpl::FindBufferResourceForHandle(record->Desc.BufferForTexBuffer)) {
|
||||
buffer->serverGpuWritten = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (g_writableImageBufferUnitCount == 0) return;
|
||||
for (Uint unit = 0; unit < g_writableImageBufferUnits.size(); ++unit) {
|
||||
@@ -8491,6 +8545,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return g_GLESCapabilities.SupportsDrawIndirect;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-C: the command bytes an indirect executor is about to CONSUME on the CPU, made current.
|
||||
// Consumed means: every field on the CPU path (no native indirect), or on the native path the
|
||||
// uniforms fed from the command - gl_BaseVertex whenever the program reads it, and
|
||||
// gl_BaseInstance when the program reads it without the mg_IndirectParams storage view that
|
||||
// would give it the GPU's word. A native draw that feeds neither reads nothing and keeps its
|
||||
// pointer, so a compute-culled indirect buffer drawn every frame costs no readback.
|
||||
static const Uint8* CurrentIndirectCommandBytes(MG_Pipe::MGPipeHandle handle, const Uint8* commandBytes,
|
||||
SizeT commandOffset, Bool useNative, Bool indexed) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return commandBytes;
|
||||
if (useNative) {
|
||||
const auto program = GetCurrentBackendProgram();
|
||||
if (program == nullptr) return commandBytes;
|
||||
const Bool baseVertexFromCpu = indexed && program->ReadsBaseVertex();
|
||||
const Bool baseInstanceFromCpu = program->GetIndirectParamsBinding() < 0 && program->ReadsBaseInstance();
|
||||
if (!baseVertexFromCpu && !baseInstanceFromCpu) return commandBytes;
|
||||
}
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
|
||||
if (resource == nullptr) return commandBytes;
|
||||
const Uint8* base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_fields");
|
||||
return base != nullptr ? base + commandOffset : commandBytes;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
|
||||
// execute natively on the GPU so commands written by compute shaders (e.g. Flywheel's
|
||||
// culling pipeline updating instanceCount) are honored; the CPU shadow is still consulted
|
||||
@@ -8537,6 +8615,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
:
|
||||
#endif
|
||||
drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative,
|
||||
/*indexed=*/true);
|
||||
#endif
|
||||
if (useNative) {
|
||||
// gl_BaseInstance must observe GPU-written command fields; expose the indirect
|
||||
// buffer to the program's mg_IndirectParams SSBO view and address it per draw.
|
||||
@@ -8635,6 +8717,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
:
|
||||
#endif
|
||||
drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative,
|
||||
/*indexed=*/false);
|
||||
#endif
|
||||
if (useNative) {
|
||||
const auto backendProgram = GetCurrentBackendProgram();
|
||||
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
|
||||
@@ -9219,9 +9305,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
// The staged shadow is the source of truth for CPU reads on this side: a
|
||||
// persistent map's blocks and a shader write's writeback were consumed by the
|
||||
// applier before this verb ran.
|
||||
const Uint8* hostBytes = elementResource ? BufferImpl::SplitHostBytes(*elementResource) : nullptr;
|
||||
// persistent map's blocks were consumed by the applier before this verb ran, and
|
||||
// (P8-C) a store a shader wrote on this side is read back from this server's own
|
||||
// GL buffer first - no writeback ever reaches the server's copy.
|
||||
const Uint8* hostBytes =
|
||||
elementResource
|
||||
? BufferImpl::SplitHostBytesForCpuRead(*elementResource, elements,
|
||||
"primitive_restart_substitution")
|
||||
: nullptr;
|
||||
if (hostBytes == nullptr) {
|
||||
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of "
|
||||
"the bound element array buffer and none is available.",
|
||||
@@ -9538,8 +9629,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
|
||||
auto* paramResource =
|
||||
BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
|
||||
const Uint8* const drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
|
||||
const Uint8* const parameterBytes = paramResource ? BufferImpl::SplitHostBytes(*paramResource) : nullptr;
|
||||
// P8-C: the count is CONSUMED here, so the parameter buffer is made current first
|
||||
// (a store a shader wrote on this side is read back); the command buffer only gets a
|
||||
// first copy when it has none - its fields are made current by the executor, and
|
||||
// only when it consumes them (CurrentIndirectCommandBytes).
|
||||
const Uint8* const parameterBytes =
|
||||
paramResource ? BufferImpl::SplitHostBytesForCpuRead(*paramResource,
|
||||
applierState.VerbIndirectParameterBuffer,
|
||||
"indirect_count_parameter")
|
||||
: nullptr;
|
||||
const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
|
||||
if (drawBytes == nullptr && drawResource != nullptr) {
|
||||
drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer,
|
||||
"indirect_count_commands");
|
||||
}
|
||||
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
return;
|
||||
@@ -9692,8 +9795,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
|
||||
auto* paramResource =
|
||||
BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
|
||||
const Uint8* const drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
|
||||
const Uint8* const parameterBytes = paramResource ? BufferImpl::SplitHostBytes(*paramResource) : nullptr;
|
||||
// P8-C: the count is CONSUMED here, so the parameter buffer is made current first
|
||||
// (a store a shader wrote on this side is read back); the command buffer only gets a
|
||||
// first copy when it has none - its fields are made current by the executor, and
|
||||
// only when it consumes them (CurrentIndirectCommandBytes).
|
||||
const Uint8* const parameterBytes =
|
||||
paramResource ? BufferImpl::SplitHostBytesForCpuRead(*paramResource,
|
||||
applierState.VerbIndirectParameterBuffer,
|
||||
"indirect_count_parameter")
|
||||
: nullptr;
|
||||
const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
|
||||
if (drawBytes == nullptr && drawResource != nullptr) {
|
||||
drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer,
|
||||
"indirect_count_commands");
|
||||
}
|
||||
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
return;
|
||||
|
||||
@@ -2304,6 +2304,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGL_SERVER_STAGED_DROP(*resource);
|
||||
resource->hostBytes = nullptr;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-C: whatever a shader wrote belonged to the store this call replaces.
|
||||
resource->serverGpuWritten = false;
|
||||
#endif
|
||||
}
|
||||
if (!resource) return; // lazy: the ensure path full-uploads on creation
|
||||
if (resource->immutableStorage) {
|
||||
@@ -3554,6 +3558,81 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const auto* record = ResourceRecordOf(res);
|
||||
return record != nullptr && record->Desc.HasDefinedContent != 0;
|
||||
}
|
||||
|
||||
// P8-C. The declaration says what; this says in what order, and it is Ops_H_Readback's
|
||||
// order because it is the same question asked by the server of itself instead of by the
|
||||
// client: (1) queued uploads land in the GL store first - reading back past them would
|
||||
// revert them in the copy - under the same staged-coverage assertion that drain carries;
|
||||
// (2) queued resident writes land (GPU-ordered, behind every command already recorded);
|
||||
// (3) the whole store is read back into the staged copy, which leaves its coverage whole
|
||||
// and so also answers RequireStagedCoverage for an orphaned store's first copy; (4) the
|
||||
// serial stamp and the epoch bump, as Ops_H_ReadbackTracked does them.
|
||||
const Uint8* SplitHostBytesForCpuRead(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
|
||||
const char* site) {
|
||||
Bool residentWritesQueued = false;
|
||||
if (resource.externalAhb != nullptr) {
|
||||
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
||||
residentWritesQueued = !resource.pendingResidentWrites.empty();
|
||||
}
|
||||
if (!resource.serverGpuWritten && !residentWritesQueued) return SplitHostBytes(resource);
|
||||
// The marks are made only under a transport, where the staged store COPIES; the guard
|
||||
// keeps a monolith store (which keeps the caller's pointer) from adopting a mapping.
|
||||
if (!ServerStaged().CopiesIntoServerStorage()) return SplitHostBytes(resource);
|
||||
if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized ||
|
||||
resource.pendingRespecify || resource.contextGeneration != g_bufferContextGeneration ||
|
||||
!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) {
|
||||
MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU but its GL store cannot be read "
|
||||
"back here, so the CPU reads the staged copy as it is",
|
||||
site, res.Slot, res.Gen);
|
||||
return SplitHostBytes(resource);
|
||||
}
|
||||
if (resource.externalAhb != nullptr) {
|
||||
// T0: the copy IS this server's coherent map of the client's pages. Landing the
|
||||
// queued writes and finishing makes them, and every shader write already queued,
|
||||
// visible through it - Ops_H_Readback's persistent-map arm, for the same reason.
|
||||
DrainResidentWritesNow(resource);
|
||||
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
|
||||
resource.serverGpuWritten = false;
|
||||
return SplitHostBytes(resource);
|
||||
}
|
||||
const SizeT width = ResourceWidthOf(res);
|
||||
const SizeT size = std::min(width, resource.storageSize);
|
||||
if (size == 0) return SplitHostBytes(resource);
|
||||
MGL_SERVER_STAGED_REQUIRE_PENDING(resource, resource.hostBytes, width, "gpu_written_refresh_flush_pending");
|
||||
FlushPendingRangesFrom(resource, resource.hostBytes, width);
|
||||
DrainResidentWritesNow(resource);
|
||||
BindBufferId(TempBufferTarget, resource.id);
|
||||
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
|
||||
GL_MAP_READ_BIT);
|
||||
if (mapped == nullptr) {
|
||||
MGLOG_E_ONCE("%s: glMapBufferRange(read) failed for GPU-written buffer %u; the CPU reads the "
|
||||
"staged copy as it is",
|
||||
site, resource.id);
|
||||
return SplitHostBytes(resource);
|
||||
}
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
||||
resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, width, mapped, 0, size);
|
||||
}
|
||||
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
|
||||
resource.serverGpuWritten = false;
|
||||
resource.syncedChangeSerial = ResourceSerialOf(res);
|
||||
BumpBufferMutationEpoch();
|
||||
MGLOG_D("%s: refreshed the staged copy of GPU-written buffer {%u, %u} (%zu bytes)", site, res.Slot,
|
||||
res.Gen, size);
|
||||
return resource.hostBytes;
|
||||
}
|
||||
|
||||
void StageServerWrittenRange(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, const void* bytes,
|
||||
SizeT offset, SizeT size) {
|
||||
if (bytes == nullptr || size == 0 || resource.externalAhb != nullptr) return;
|
||||
// Only a COPYING store may take bytes that die with a mapping: the monolith one keeps
|
||||
// the caller's pointer (StagedShadow.h), which here would dangle after the unmap.
|
||||
if (!ServerStaged().CopiesIntoServerStorage()) return;
|
||||
// Under pendingMutex, as every other hostBytes store is (M-2).
|
||||
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
||||
resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, ResourceWidthOf(res), bytes, offset, size);
|
||||
}
|
||||
#endif
|
||||
|
||||
MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject) {
|
||||
@@ -6589,10 +6668,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// path, and a persistently mapped source is excluded separately below (the memo
|
||||
// never trusts one, so those re-read every draw through the coherent map).
|
||||
//
|
||||
// P8 closes it by moving the pull to the client, where the object lives. Until then
|
||||
// this is the ONE behavioural difference between the two arms under the default
|
||||
// mask, and it is written here rather than only in a document so that the next
|
||||
// reader of this function finds it at the site.
|
||||
// P8-C CLOSES IT UNDER A TRANSPORT, on the server rather than by moving the pull to the
|
||||
// client: a source a shader wrote on this side is read back from this server's own GL
|
||||
// buffer (SplitHostBytesForCpuRead), and that write also voids the memo below, whose
|
||||
// serial a GPU write never moves. The MONOLITH arm of this function keeps the
|
||||
// deviation (its hostBytes is the client's shadow and its mark is the frontend
|
||||
// object's); that half is dev's (ID-P8-3).
|
||||
// WHERE THE BYTES ARE, and it is not one fixed place: an ADOPTED store has no client
|
||||
// shadow left at all (PipeResource::AdoptPersistentMap clears and shrinks it), so
|
||||
// the coherent map IS the source of truth - which is exactly the case the memo
|
||||
@@ -6601,6 +6682,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// recorded shadow base for such a resource read freed memory; hostBytes is nulled
|
||||
// at adoption for the same reason, so the fallback below is a null and a refusal
|
||||
// rather than a stale read.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const Bool sourceGpuWritten =
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith && resource->serverGpuWritten;
|
||||
if (sourceGpuWritten) {
|
||||
BufferImpl::SplitHostBytesForCpuRead(*resource, binding.Res, "fp64_vertex_narrowing");
|
||||
}
|
||||
#else
|
||||
constexpr Bool sourceGpuWritten = false;
|
||||
#endif
|
||||
const Uint8* const sourceBase =
|
||||
(resource->persistentMapped && resource->persistentPtr != nullptr)
|
||||
? static_cast<const Uint8*>(resource->persistentPtr)
|
||||
@@ -6637,7 +6727,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// still never trusted - it is written through the pointer with no call at all, so
|
||||
// no serial on either side can prove the converted copy is current.
|
||||
const Bool memoHit = stream.valid && convertedBufferId != 0 && !resource->persistentMapped &&
|
||||
stream.sourceHandle == binding.Res &&
|
||||
!sourceGpuWritten && stream.sourceHandle == binding.Res &&
|
||||
stream.sourceChangeSerial == sourceSerial &&
|
||||
stream.sourceOffset == static_cast<SizeT>(attrib.Offset) &&
|
||||
stream.sourceStride == sourceStride &&
|
||||
|
||||
@@ -1051,6 +1051,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// store's last use has signalled (RetireT0Import). Survives an ES context loss:
|
||||
// the next ensure re-imports it. Null for every store T0 did not adopt.
|
||||
void* externalAhb = nullptr;
|
||||
// P8-C: A SHADER WROTE THIS STORE ON THIS SIDE since the R-11 staged copy (or, for
|
||||
// T0, the coherent map) was last known to match it - set by the draw / dispatch that
|
||||
// bound it writable (storage block, atomic counter, writable buffer image), cleared
|
||||
// by SplitHostBytesForCpuRead's refresh and by a respecify. Only the server's own
|
||||
// CPU readers consult it; the client keeps its own set (GpuWritePending.h).
|
||||
Bool serverGpuWritten = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -1123,6 +1129,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return resource.externalAhb != nullptr ? static_cast<const Uint8*>(resource.persistentPtr) : nullptr;
|
||||
}
|
||||
|
||||
// P8-C: THE SAME BYTES, MADE CURRENT FIRST - for a reader that CONSUMES them (the restart
|
||||
// rewrite, the multi-draw rebase, the *IndirectCount count, an indirect command's
|
||||
// CPU-fed fields, the XFB scatter's pre-capture bytes). SplitHostBytes above is only
|
||||
// "what uploads staged": a store a shader wrote on this side (serverGpuWritten) is
|
||||
// refreshed from this server's own GL buffer - queued uploads flushed into it first,
|
||||
// then the whole store read back into the staged copy, which also gives an orphaned
|
||||
// store its first copy - and a T0 store lands its queued resident writes and finishes
|
||||
// before its coherent map is read. A clean staged store costs one flag test.
|
||||
// `res` names the record whose descriptor width the copy takes; `site` names the
|
||||
// reader in the one-time diagnostic when the refresh cannot run.
|
||||
const Uint8* SplitHostBytesForCpuRead(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
|
||||
const char* site);
|
||||
// P8-C: bytes the SERVER itself put into a store's GL buffer and already holds on the CPU
|
||||
// (an XFB capture it mapped for the client's writeback, a scatter it composed) go into
|
||||
// the staged copy as well, so no refresh is owed for them. A T0 store has no copy.
|
||||
void StageServerWrittenRange(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, const void* bytes,
|
||||
SizeT offset, SizeT size);
|
||||
|
||||
// M-3's WHOLE-STORE predicate, and the GUARD every caller of RequireStagedCoverage over
|
||||
// a whole-store read has to carry: true when the application DECLARED the store's
|
||||
// content (glBufferData(size, data)), so a coverage gap is a missing record; false when
|
||||
@@ -3079,6 +3103,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
||||
// uniform write, and only such a program needs the reset after one.
|
||||
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-C: same for the gl_BaseInstance uniform - which an indirect draw feeds from the
|
||||
// command on the CPU only when the mg_IndirectParams view is absent.
|
||||
Bool ReadsBaseInstance() const { return m_baseInstanceUniformLocation >= 0; }
|
||||
#endif
|
||||
// Which viewport indices the next draw's fragments may keep, one bit each. Written
|
||||
// once per replay pass; see ForEachViewportRoutingPass.
|
||||
void SetViewportPassMask(Uint32 indexMask) const;
|
||||
|
||||
@@ -191,15 +191,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (question == IndexBufferQuestion::Presence) return view;
|
||||
|
||||
if (question == IndexBufferQuestion::HostBytes) {
|
||||
// No Sync* pair here and none is missing: on this side the applier has
|
||||
// already consumed the persistent-map blocks and the shader writebacks for
|
||||
// this resource before the draw verb replayed, so the server's staged shadow
|
||||
// IS the synced copy. That is also why this arm cannot be expressed as "find
|
||||
// the object and run the monolith body".
|
||||
// The applier has already consumed the persistent-map blocks for this
|
||||
// resource before the draw verb replayed; the one Sync* this arm owes is the
|
||||
// monolith's SyncGpuWrites, and (P8-C) SplitHostBytesForCpuRead is it: a store
|
||||
// a shader wrote on this side is read back from this server's own GL buffer,
|
||||
// because no writeback ever reaches the server's staged copy. That is also why
|
||||
// this arm cannot be expressed as "find the object and run the monolith body".
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
|
||||
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
view.HostBytes = BufferImpl::SplitHostBytes(*resource);
|
||||
view.HostBytes = BufferImpl::SplitHostBytesForCpuRead(*resource, res, "multidraw_index_rebase");
|
||||
if (view.HostBytes != nullptr && IndexBufferRecordHasDefinedContent(st, res)) {
|
||||
BufferImpl::RequireStagedCoverage(*resource, view.HostBytes, 0, view.Size,
|
||||
"multidraw_index_rebase");
|
||||
|
||||
@@ -3391,6 +3391,14 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
endforeach()
|
||||
endforeach()
|
||||
|
||||
# P8-C: draw inputs a compute shader wrote, read on the CPU by the side that applies the draw
|
||||
# (restart rewrite, multi-draw rebase, *IndirectCount, indirect gl_BaseVertex, an orphaned
|
||||
# indirect store). Split-only source, like F1: the ambient DirectGLES./DirectVulkan. entries
|
||||
# of the split build are the monolith control and the inproc A/B; this call adds the three
|
||||
# gated Espryt arms and records the Magma tier-2 replay (integration-magma-all-*).
|
||||
target_sources(MobileGLIntegrationTest PRIVATE Scenarios/GpuWrittenDrawInputScenario.cpp)
|
||||
mgl_itest_register_split_arms("GpuWrittenDrawInputScenario.*" "")
|
||||
|
||||
# The configure_file that used to sit here MOVED TO THE END OF THIS FILE (P6 `t6`).
|
||||
#
|
||||
# It substitutes @MGL_SPLIT_ARM_TEST_LISTS@, which the registration macro appends to - so
|
||||
|
||||
@@ -0,0 +1,800 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.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
|
||||
//
|
||||
// Scenario - DRAW INPUTS A SHADER WROTE, READ BY A BACKEND ON THE CPU (P8-C).
|
||||
//
|
||||
// Espryt emulates five desktop-GL draw features on the CPU, and each one reads a buffer's bytes
|
||||
// on the side that applies the draw:
|
||||
//
|
||||
// * the primitive-restart rewrite (an application restart index that is not the all-ones value
|
||||
// of the index type): the whole element array buffer, rewritten into a scratch copy;
|
||||
// * the multi-draw rebase tier, which a batch with such a restart index is forced onto;
|
||||
// * *IndirectCount: the count word of the parameter buffer (GLES has no counted indirect draw);
|
||||
// * a native indirect draw's gl_BaseVertex, fed to the shader as a uniform from the command;
|
||||
// * a GL_DOUBLE vertex array, narrowed to float from the source buffer's bytes.
|
||||
//
|
||||
// Under split those bytes are the server's R-11 staged copy, which only uploads fill. A buffer a
|
||||
// shader WROTE on the server - storage block, atomic counter, buffer image, transform feedback -
|
||||
// is therefore read stale unless the server refreshes its copy first. Every case below writes
|
||||
// the buffer on the GPU only (the staged copy holds a poison the correct picture cannot come
|
||||
// from), so a stale read is a wrong picture, never a lucky one.
|
||||
//
|
||||
// MONOLITH ARMS KNOWN BROKEN HERE SKIP BY NAME (SkipBrokenMonolith). Espryt reads three from the
|
||||
// frontend shadow without SyncGpuWrites: *IndirectCount (OPEN-QUESTIONS 15), an indirect
|
||||
// gl_BaseVertex (the comment beside it in DirectGLES.cpp says so) and a GL_DOUBLE vertex array
|
||||
// (the M-3 deviation declared in Managers.cpp). Magma reads MultiDrawArraysIndirectCount's count
|
||||
// the same way, and its restart rewrite and GL_DOUBLE conversion call SyncGpuWrites while the
|
||||
// draw is being recorded, so the draw then records into an ended command buffer (SIGSEGV in
|
||||
// lavapipe). Those arms are dev's to fix (ID-P8-3); the skip keeps the ambient monolith lane
|
||||
// honest without hiding the split arms, which run the same cases under DirectGLES.Split/Spawn/Tcp,
|
||||
// DirectVulkan.*.Full and the inproc A/B.
|
||||
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.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 int kSurface = 64;
|
||||
|
||||
constexpr const char* kFlatVertex = R"(#version 430 core
|
||||
layout(location = 0) in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFlatFragment = R"(#version 430 core
|
||||
out vec4 oColor;
|
||||
void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// Green when the shader saw gl_BaseVertex == 9 (the value the compute shader wrote),
|
||||
// red for anything else - in particular 3, the value the staged copy still holds.
|
||||
constexpr const char* kBaseVertexVertex = R"(#version 450 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
layout(location = 0) in vec2 aPos;
|
||||
flat out vec4 vColor;
|
||||
void main() {
|
||||
vColor = gl_BaseVertexARB == 9 ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kBaseVertexFragment = R"(#version 450 core
|
||||
flat in vec4 vColor;
|
||||
out vec4 oColor;
|
||||
void main() { oColor = vColor; }
|
||||
)";
|
||||
|
||||
// ---- the restart scene (PrimitiveRestartScenario's geometry) ----
|
||||
//
|
||||
// Two triangles with a gap between them, plus two spare vertices at the origin. Index 7 is
|
||||
// the application's restart index and a legal vertex, so a dropped restart welds the strip
|
||||
// across the gap (caught by the gap probe) instead of reading out of bounds.
|
||||
constexpr GLfloat kRestartVertices[] = {
|
||||
-0.9f, -0.9f, -0.1f, -0.9f, -0.9f, 0.9f, // 0-2: left triangle
|
||||
0.1f, -0.9f, 0.9f, -0.9f, 0.9f, 0.9f, // 3-5: right triangle
|
||||
0.0f, 0.0f, 0.0f, 0.0f, // 6, 7: spares
|
||||
};
|
||||
constexpr GLuint kRestartIndex = 7;
|
||||
constexpr GLuint kRestartIndexCount = 7;
|
||||
// What the compute shader writes: {0, 1, 2, 7, 3, 4, 5}.
|
||||
constexpr const char* kWriteRestartIndices = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Indices { uint value[]; };
|
||||
void main() {
|
||||
value[0] = 0u; value[1] = 1u; value[2] = 2u; value[3] = 7u;
|
||||
value[4] = 3u; value[5] = 4u; value[6] = 5u;
|
||||
}
|
||||
)";
|
||||
// The staged poison: every index names spare vertex 6, a strip of degenerate triangles
|
||||
// that paints nothing.
|
||||
const std::vector<GLuint> kRestartPoison(kRestartIndexCount, 6u);
|
||||
|
||||
// The same seven indices, produced by a TRANSFORM FEEDBACK capture: one point per index.
|
||||
constexpr const char* kCaptureIndicesVertex = R"(#version 430 core
|
||||
flat out uint idx;
|
||||
const uint kTable[7] = uint[7](0u, 1u, 2u, 7u, 3u, 4u, 5u);
|
||||
void main() {
|
||||
idx = kTable[gl_VertexID];
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 1.0;
|
||||
}
|
||||
)";
|
||||
// ...and by a capture with a HOLE after every index (gl_SkipComponents1), which ES cannot
|
||||
// express and Espryt therefore scatters itself. The captured words land at the even
|
||||
// slots; the odd slots keep what the application staged there.
|
||||
constexpr const char* kCaptureScatteredVertex = R"(#version 430 core
|
||||
flat out uint idx;
|
||||
const uint kTable[4] = uint[4](0u, 2u, 3u, 5u);
|
||||
void main() {
|
||||
idx = kTable[gl_VertexID];
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 1.0;
|
||||
}
|
||||
)";
|
||||
constexpr const char* kCaptureFragment = R"(#version 430 core
|
||||
out vec4 oColor;
|
||||
void main() { oColor = vec4(1.0); }
|
||||
)";
|
||||
// Even slots are the capture's (poison 6 until it lands), odd slots the holes: after the
|
||||
// scatter the first seven words are {0, 1, 2, 7, 3, 4, 5}; before it, a strip of
|
||||
// degenerate triangles.
|
||||
constexpr GLuint kScatterStaged[8] = {6u, 1u, 6u, 7u, 6u, 4u, 6u, 6u};
|
||||
|
||||
// ...and by IMAGE STORES into a buffer texture over the element array buffer.
|
||||
constexpr const char* kImageStoreRestartIndices = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(r32ui, binding = 0) uniform writeonly uimageBuffer indices;
|
||||
const uint kTable[7] = uint[7](0u, 1u, 2u, 7u, 3u, 4u, 5u);
|
||||
void main() {
|
||||
for (int i = 0; i < 7; ++i) imageStore(indices, i, uvec4(kTable[i]));
|
||||
}
|
||||
)";
|
||||
|
||||
// NDC (-0.5, -0.5), (0.6, -0.5), (0.2, -0.5) on the 64x64 surface.
|
||||
constexpr int kLeftX = 16, kRightX = 51, kGapX = 38, kProbeY = 16;
|
||||
|
||||
// ---- the indirect scene (DrawParametersScenario's geometry) ----
|
||||
//
|
||||
// Three pad vertices at the origin, then the left half of the surface as two triangles
|
||||
// (vertices 3-8), then the right half (9-14). Element indices 0..5 address one half
|
||||
// through the command's baseVertex.
|
||||
constexpr int kLeftFirst = 3;
|
||||
constexpr int kRightFirst = 9;
|
||||
constexpr int kHalfCount = 6;
|
||||
|
||||
std::vector<GLfloat> HalvesVertices() {
|
||||
std::vector<GLfloat> v(6, 0.0f);
|
||||
const float bounds[2][2] = {{-1.0f, 0.0f}, {0.0f, 1.0f}};
|
||||
for (const auto& half : bounds) {
|
||||
const float x0 = half[0];
|
||||
const float x1 = half[1];
|
||||
const float quad[] = {x0, -1.0f, x1, -1.0f, x1, 1.0f, x0, -1.0f, x1, 1.0f, x0, 1.0f};
|
||||
v.insert(v.end(), std::begin(quad), std::end(quad));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
struct ElementsCommand {
|
||||
std::uint32_t count, instanceCount, firstIndex;
|
||||
std::int32_t baseVertex;
|
||||
std::uint32_t baseInstance;
|
||||
};
|
||||
struct ArraysCommand {
|
||||
std::uint32_t count, instanceCount, first, baseInstance;
|
||||
};
|
||||
|
||||
bool IsGreen(const Rgba8& p) { return p.g > 128 && p.r < 128; }
|
||||
|
||||
class GpuWrittenDrawInputScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_target = MakeColorFbo(kSurface, kSurface);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "the 64x64 colour target did not build";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(0);
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0);
|
||||
glBindBuffer(GL_PARAMETER_BUFFER, 0);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
m_texture = 0;
|
||||
for (GLuint* buffer : {&m_vbo, &m_ebo, &m_commands, &m_parameters}) {
|
||||
if (*buffer != 0) glDeleteBuffers(1, buffer);
|
||||
*buffer = 0;
|
||||
}
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
for (GLuint program : m_programs) glDeleteProgram(program);
|
||||
m_programs.clear();
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_target);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// THE ARMS THIS FILE SKIPS, BY NAME: the monolith arm of a backend whose reason is
|
||||
// non-null. Every split arm, and the inproc A/B, runs the case.
|
||||
bool SkipBrokenMonolith(const char* espryt, const char* magma) {
|
||||
if (PeekSplitRuntime().transportName != "monolith") return false;
|
||||
const std::string backend = Gl().BackendName();
|
||||
const char* why = backend == "DirectGLES" ? espryt : backend == "DirectVulkan" ? magma : nullptr;
|
||||
if (why == nullptr) return false;
|
||||
m_skipReason = "monolith " + backend + ": " + why +
|
||||
" (dev's fix, ID-P8-3) - this case gates the split arms";
|
||||
return true;
|
||||
}
|
||||
static constexpr const char* kUnsyncedCount =
|
||||
"the *IndirectCount count is read from the frontend shadow without SyncGpuWrites "
|
||||
"(OPEN-QUESTIONS 15)";
|
||||
static constexpr const char* kUnsyncedBaseVertex =
|
||||
"an indirect command's gl_BaseVertex is read from the frontend shadow without "
|
||||
"SyncGpuWrites";
|
||||
static constexpr const char* kUnsyncedDoubles =
|
||||
"a GL_DOUBLE vertex array is narrowed from the client shadow without SyncGpuWrites "
|
||||
"(the M-3 deviation declared in Managers.cpp)";
|
||||
static constexpr const char* kSyncMidRecording =
|
||||
"SyncGpuWrites inside the draw (the restart rewrite, the GL_DOUBLE vertex conversion) "
|
||||
"submits the frame mid-recording and the draw then records into the ended command "
|
||||
"buffer (SIGSEGV)";
|
||||
|
||||
GLuint Program(const char* vertex, const char* fragment) {
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(vertex, fragment, &error);
|
||||
EXPECT_NE(program, 0u) << error;
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint ComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
char log[4096]{};
|
||||
glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||
ADD_FAILURE() << log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (!linked) {
|
||||
char log[4096]{};
|
||||
glGetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
ADD_FAILURE() << log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// Buffer `name` at `target` with `bytes` bytes of `data` (null: an orphaned store).
|
||||
static void Store(GLuint& name, GLenum target, GLsizeiptr bytes, const void* data) {
|
||||
if (name == 0) glGenBuffers(1, &name);
|
||||
glBindBuffer(target, name);
|
||||
glBufferData(target, bytes, data, GL_DYNAMIC_COPY);
|
||||
}
|
||||
|
||||
void BindVertices(const GLfloat* vertices, GLsizeiptr bytes) {
|
||||
glBindVertexArray(m_vao);
|
||||
Store(m_vbo, GL_ARRAY_BUFFER, bytes, vertices);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);
|
||||
}
|
||||
|
||||
// One dispatch of `source` with `first` (and `second`, if non-zero) bound as storage
|
||||
// buffers 0 and 1, then the barrier the draw that consumes them needs.
|
||||
void WriteWithCompute(const char* source, GLuint first, GLuint second, GLbitfield barrier) {
|
||||
const GLuint compute = ComputeProgram(source);
|
||||
ASSERT_NE(compute, 0u);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, first);
|
||||
if (second != 0) glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, second);
|
||||
glUseProgram(compute);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(barrier | GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
template <typename DrawFn>
|
||||
Image Render(GLuint program, DrawFn&& draw) {
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(m_vao);
|
||||
draw();
|
||||
return ReadPixels(kSurface, kSurface);
|
||||
}
|
||||
|
||||
// The element array buffer of the restart scene, written by a compute shader over the
|
||||
// poison (or over an orphaned store when `orphaned`).
|
||||
void BuildRestartScene(bool orphaned) {
|
||||
BindVertices(kRestartVertices, sizeof(kRestartVertices));
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, kRestartIndexCount * sizeof(GLuint),
|
||||
orphaned ? nullptr : kRestartPoison.data());
|
||||
WriteWithCompute(kWriteRestartIndices, m_ebo, 0, GL_ELEMENT_ARRAY_BARRIER_BIT);
|
||||
ArmRestart();
|
||||
}
|
||||
|
||||
// The element array buffer bound to the scene's VAO, restart index 7 on.
|
||||
void ArmRestart() {
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
}
|
||||
|
||||
// A vertex + fragment program whose `varyings` are captured INTERLEAVED.
|
||||
GLuint XfbProgram(const char* vertex, const char* fragment, const char* const* varyings,
|
||||
GLsizei count) {
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &vertex, nullptr);
|
||||
glCompileShader(vs);
|
||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fs, 1, &fragment, nullptr);
|
||||
glCompileShader(fs);
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glTransformFeedbackVaryings(program, count, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (!linked) {
|
||||
char log[4096]{};
|
||||
glGetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
ADD_FAILURE() << "the capture program did not link: " << log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// `vertices` points drawn with rasterization off, captured into m_ebo from byte 0.
|
||||
void CaptureIntoElementBuffer(GLuint program, GLsizei vertices) {
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_ebo);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, vertices);
|
||||
glEndTransformFeedback();
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
static void ExpectSplitStrip(const Image& image, const char* what) {
|
||||
EXPECT_TRUE(IsGreen(image.At(kLeftX, kProbeY)))
|
||||
<< what << ": the left triangle did not render (" << image.At(kLeftX, kProbeY)
|
||||
<< ") - the backend drew the indices it had staged, not the ones the compute "
|
||||
"shader wrote";
|
||||
EXPECT_TRUE(IsGreen(image.At(kRightX, kProbeY)))
|
||||
<< what << ": the right triangle did not render (" << image.At(kRightX, kProbeY) << ")";
|
||||
EXPECT_FALSE(IsGreen(image.At(kGapX, kProbeY)))
|
||||
<< what << ": the gap is covered, so the restart at index 7 was dropped";
|
||||
}
|
||||
|
||||
// Left and right half of the halves scene, sampled at their centres.
|
||||
static Rgba8 Half(const Image& image, int half) {
|
||||
return image.At(kSurface * (1 + 2 * half) / 4, kSurface / 2);
|
||||
}
|
||||
|
||||
ColorFbo m_target;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_ebo = 0;
|
||||
GLuint m_commands = 0;
|
||||
GLuint m_parameters = 0;
|
||||
GLuint m_texture = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_skipReason;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- the primitive-restart rewrite (DirectGLES.cpp ScopedRestartIndexSubstitution) ----
|
||||
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAComputeShaderWroteSplitTheStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
BuildRestartScene(/*orphaned=*/false);
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glDrawElements over compute-written restart indices");
|
||||
}
|
||||
|
||||
// The same indices in a store the application ORPHANED (glBufferData(NULL)): nothing was
|
||||
// ever staged for it, so the only copy of its bytes is the one the GPU holds.
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAComputeShaderWroteIntoAnOrphanedStoreSplitTheStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
BuildRestartScene(/*orphaned=*/true);
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glDrawElements over compute-written restart indices in an orphaned store");
|
||||
}
|
||||
|
||||
// ---- the multi-draw rebase tier (MultiDraw.cpp RunRebasedDrawElements) ----
|
||||
//
|
||||
// An arbitrary restart index forces a multi-draw onto the one tier that rewrites the stream,
|
||||
// and that tier reads the whole element array buffer on the CPU. Sub-draw 0 is {0,1,2} (the
|
||||
// left triangle); sub-draw 1 is {7,3,4,5} (a restart, then the right triangle).
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAComputeShaderWroteSplitEveryMultiDrawStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
BuildRestartScene(/*orphaned=*/false);
|
||||
const GLsizei counts[2] = {3, 4};
|
||||
const void* offsets[2] = {nullptr, reinterpret_cast<const void*>(3 * sizeof(GLuint))};
|
||||
const Image image = Render(program, [&] {
|
||||
glMultiDrawElements(GL_TRIANGLE_STRIP, counts, GL_UNSIGNED_INT, offsets, 2);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glMultiDrawElements over compute-written restart indices");
|
||||
}
|
||||
|
||||
// ---- *IndirectCount (DirectGLES.cpp MultiDraw{Elements,Arrays}IndirectCount) ----
|
||||
//
|
||||
// The compute shader writes BOTH commands and the count; the staged copies hold zeroes, so a
|
||||
// stale count draws nothing and a stale command (on a device without native indirect) draws
|
||||
// nothing either.
|
||||
TEST_F(GpuWrittenDrawInputScenario, ACountAComputeShaderWroteDrivesMultiDrawElementsIndirectCount) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(kUnsyncedCount, nullptr)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const std::vector<GLfloat> vertices = HalvesVertices();
|
||||
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
|
||||
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
|
||||
const ElementsCommand zeroCommands[2] = {};
|
||||
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(zeroCommands), zeroCommands);
|
||||
const GLuint zeroCount = 0;
|
||||
Store(m_parameters, GL_PARAMETER_BUFFER, sizeof(zeroCount), &zeroCount);
|
||||
WriteWithCompute(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Commands { uint command[]; };
|
||||
layout(std430, binding = 1) buffer Parameters { uint count; };
|
||||
void main() {
|
||||
command[0] = 6u; command[1] = 1u; command[2] = 0u; command[3] = 3u; command[4] = 0u;
|
||||
command[5] = 6u; command[6] = 1u; command[7] = 0u; command[8] = 9u; command[9] = 0u;
|
||||
count = 2u;
|
||||
}
|
||||
)",
|
||||
m_commands, m_parameters, GL_COMMAND_BARRIER_BIT);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
|
||||
glBindBuffer(GL_PARAMETER_BUFFER, m_parameters);
|
||||
const Image image = Render(program, [] {
|
||||
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 0, 2, 0);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0)))
|
||||
<< "command 0 did not draw (" << Half(image, 0)
|
||||
<< "): the count was read from a copy the compute shader never reached";
|
||||
EXPECT_TRUE(IsGreen(Half(image, 1))) << "command 1 did not draw (" << Half(image, 1) << ")";
|
||||
}
|
||||
|
||||
TEST_F(GpuWrittenDrawInputScenario, ACountAComputeShaderWroteDrivesMultiDrawArraysIndirectCount) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(kUnsyncedCount, kUnsyncedCount)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const std::vector<GLfloat> vertices = HalvesVertices();
|
||||
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
|
||||
const ArraysCommand zeroCommands[2] = {};
|
||||
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(zeroCommands), zeroCommands);
|
||||
const GLuint zeroCount = 0;
|
||||
Store(m_parameters, GL_PARAMETER_BUFFER, sizeof(zeroCount), &zeroCount);
|
||||
WriteWithCompute(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Commands { uint command[]; };
|
||||
layout(std430, binding = 1) buffer Parameters { uint count; };
|
||||
void main() {
|
||||
command[0] = 6u; command[1] = 1u; command[2] = 3u; command[3] = 0u;
|
||||
command[4] = 6u; command[5] = 1u; command[6] = 9u; command[7] = 0u;
|
||||
count = 2u;
|
||||
}
|
||||
)",
|
||||
m_commands, m_parameters, GL_COMMAND_BARRIER_BIT);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
|
||||
glBindBuffer(GL_PARAMETER_BUFFER, m_parameters);
|
||||
const Image image = Render(program, [] {
|
||||
glMultiDrawArraysIndirectCount(GL_TRIANGLES, nullptr, 0, 2, 0);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0)))
|
||||
<< "command 0 did not draw (" << Half(image, 0)
|
||||
<< "): the count was read from a copy the compute shader never reached";
|
||||
EXPECT_TRUE(IsGreen(Half(image, 1))) << "command 1 did not draw (" << Half(image, 1) << ")";
|
||||
}
|
||||
|
||||
// ---- a native indirect draw's gl_BaseVertex (DirectGLES.cpp ExecuteIndexedIndirectCommands) ----
|
||||
//
|
||||
// The GPU executes the command itself, so the geometry is right whatever the backend's copy
|
||||
// says; gl_BaseVertex is the one field Espryt feeds from the CPU. The staged command names
|
||||
// the left half (baseVertex 3), the compute shader moves it to the right half (9).
|
||||
TEST_F(GpuWrittenDrawInputScenario, ABaseVertexAComputeShaderWroteReachesGlBaseVertex) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(kUnsyncedBaseVertex, nullptr)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kBaseVertexVertex, kBaseVertexFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const std::vector<GLfloat> vertices = HalvesVertices();
|
||||
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
|
||||
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
|
||||
const ElementsCommand staged{kHalfCount, 1, 0, kLeftFirst, 0};
|
||||
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(staged), &staged);
|
||||
WriteWithCompute(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Commands { int command[]; };
|
||||
void main() { command[3] = 9; }
|
||||
)",
|
||||
m_commands, 0, GL_COMMAND_BARRIER_BIT);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 1)))
|
||||
<< "the right half is " << Half(image, 1)
|
||||
<< ": red means the draw ran with the compute-written baseVertex but gl_BaseVertex was "
|
||||
"fed from the staged command; black means the draw did not run there at all";
|
||||
EXPECT_FALSE(IsGreen(Half(image, 0))) << "the left half drew: the GPU saw the staged command";
|
||||
}
|
||||
|
||||
// ---- indirect commands with no staged copy at all (DirectGLES.cpp ResolveIndirectCommandBytes) ----
|
||||
//
|
||||
// The command buffer is ORPHANED and only the compute shader ever writes it - the GPU-culling
|
||||
// shape. Nothing was staged for it, so a backend that needs a CPU copy before it will issue
|
||||
// the draw has to make one from its own GL buffer.
|
||||
TEST_F(GpuWrittenDrawInputScenario, IndirectCommandsAComputeShaderWroteIntoAnOrphanedStoreDraw) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const std::vector<GLfloat> vertices = HalvesVertices();
|
||||
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
|
||||
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
|
||||
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(ElementsCommand), nullptr);
|
||||
WriteWithCompute(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Commands { uint command[]; };
|
||||
void main() { command[0] = 6u; command[1] = 1u; command[2] = 0u; command[3] = 3u; command[4] = 0u; }
|
||||
)",
|
||||
m_commands, 0, GL_COMMAND_BARRIER_BIT);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0)))
|
||||
<< "the compute-written command did not draw (" << Half(image, 0)
|
||||
<< "): the backend refused a draw whose command buffer it had no CPU copy of";
|
||||
EXPECT_FALSE(IsGreen(Half(image, 1))) << "the right half drew: the command was not the one written";
|
||||
}
|
||||
|
||||
// ---- the other GPU writers: transform feedback, buffer images, atomic counters ----
|
||||
//
|
||||
// The same restart reader over indices a CAPTURE produced. Espryt maps the captured range
|
||||
// back at glEndTransformFeedback for the client; the server's own copy has to take it too.
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesATransformFeedbackCapturedSplitTheStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const char* varyings[] = {"idx"};
|
||||
const GLuint capture = XfbProgram(kCaptureIndicesVertex, kCaptureFragment, varyings, 1);
|
||||
ASSERT_NE(capture, 0u);
|
||||
BindVertices(kRestartVertices, sizeof(kRestartVertices));
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, kRestartIndexCount * sizeof(GLuint), kRestartPoison.data());
|
||||
CaptureIntoElementBuffer(capture, kRestartIndexCount);
|
||||
ArmRestart();
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glDrawElements over restart indices a transform feedback captured");
|
||||
}
|
||||
|
||||
// ...through the SCATTER Espryt runs for a layout with holes: the server composes the range
|
||||
// from its pre-capture bytes and the captured words, and its own copy has to keep the result.
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAScatteredTransformFeedbackCapturedSplitTheStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const char* varyings[] = {"idx", "gl_SkipComponents1"};
|
||||
const GLuint capture = XfbProgram(kCaptureScatteredVertex, kCaptureFragment, varyings, 2);
|
||||
ASSERT_NE(capture, 0u);
|
||||
BindVertices(kRestartVertices, sizeof(kRestartVertices));
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(kScatterStaged), kScatterStaged);
|
||||
CaptureIntoElementBuffer(capture, 4);
|
||||
ArmRestart();
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glDrawElements over restart indices a scattered capture composed");
|
||||
}
|
||||
|
||||
// ...and over indices IMAGE STORES wrote through a buffer texture on the element buffer.
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesImageStoresWroteSplitTheStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const GLuint compute = ComputeProgram(kImageStoreRestartIndices);
|
||||
ASSERT_NE(compute, 0u);
|
||||
BindVertices(kRestartVertices, sizeof(kRestartVertices));
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, kRestartIndexCount * sizeof(GLuint), kRestartPoison.data());
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, m_texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_ebo);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glBindImageTexture(0, m_texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R32UI);
|
||||
glUseProgram(compute);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ELEMENT_ARRAY_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
||||
glUseProgram(0);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
ArmRestart();
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glDrawElements over restart indices image stores wrote");
|
||||
}
|
||||
|
||||
// The GPU-culling shape at its most common: the draw count is an ATOMIC COUNTER the compute
|
||||
// shader bumped once per surviving command, and that counter buffer is the parameter buffer.
|
||||
TEST_F(GpuWrittenDrawInputScenario, ACountAnAtomicCounterProducedDrivesMultiDrawElementsIndirectCount) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(kUnsyncedCount, nullptr)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const GLuint compute = ComputeProgram(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, offset = 0) uniform atomic_uint drawCount;
|
||||
layout(std430, binding = 0) buffer Commands { uint command[]; };
|
||||
void main() {
|
||||
const uint baseVertex[2] = uint[2](3u, 9u);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
uint slot = atomicCounterIncrement(drawCount);
|
||||
command[slot * 5u + 0u] = 6u;
|
||||
command[slot * 5u + 1u] = 1u;
|
||||
command[slot * 5u + 2u] = 0u;
|
||||
command[slot * 5u + 3u] = baseVertex[i];
|
||||
command[slot * 5u + 4u] = 0u;
|
||||
}
|
||||
}
|
||||
)");
|
||||
ASSERT_NE(compute, 0u);
|
||||
const std::vector<GLfloat> vertices = HalvesVertices();
|
||||
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
|
||||
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
|
||||
const ElementsCommand zeroCommands[2] = {};
|
||||
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(zeroCommands), zeroCommands);
|
||||
const GLuint zeroCount = 0;
|
||||
Store(m_parameters, GL_ATOMIC_COUNTER_BUFFER, sizeof(zeroCount), &zeroCount);
|
||||
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, m_parameters);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_commands);
|
||||
glUseProgram(compute);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
|
||||
glBindBuffer(GL_PARAMETER_BUFFER, m_parameters);
|
||||
const Image image = Render(program, [] {
|
||||
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 0, 2, 0);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0)))
|
||||
<< "command 0 did not draw (" << Half(image, 0)
|
||||
<< "): the count was read from a copy the atomic counter never reached";
|
||||
EXPECT_TRUE(IsGreen(Half(image, 1))) << "command 1 did not draw (" << Half(image, 1) << ")";
|
||||
}
|
||||
|
||||
// ---- fp64 vertex narrowing (Managers.cpp SyncFloat64AttributeAsFloat32ByHandle) ----
|
||||
//
|
||||
// A GL_DOUBLE vertex array is narrowed to float on the CPU from the source buffer's bytes,
|
||||
// and the narrowed copy is memoised on the source's serial - which a GPU write never moves.
|
||||
// Two rounds: the first catches a stale read, the second a stale memo. The compute shader
|
||||
// writes each double as its two IEEE-754 words (low word 0), so it needs no fp64 itself.
|
||||
TEST_F(GpuWrittenDrawInputScenario, DoubleVerticesAComputeShaderWroteAreNarrowedFromTheWrittenBytes) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(kUnsyncedDoubles, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
// Four vertices, x then y, all 100.0: a strip wholly off the surface.
|
||||
const double poison[8] = {100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0};
|
||||
glBindVertexArray(m_vao);
|
||||
Store(m_vbo, GL_ARRAY_BUFFER, sizeof(poison), poison);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_DOUBLE, GL_FALSE, 2 * sizeof(double), nullptr);
|
||||
|
||||
// High words: -1.0 = 0xBFF00000, 1.0 = 0x3FF00000, 0.0 = 0.
|
||||
const auto writeQuad = [&](const char* highWords) {
|
||||
const std::string source = std::string(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Positions { uint word[]; };
|
||||
void main() {
|
||||
const uint hi[8] = uint[8]()") + highWords + R"();
|
||||
for (int i = 0; i < 8; ++i) { word[2 * i] = 0u; word[2 * i + 1] = hi[i]; }
|
||||
}
|
||||
)";
|
||||
WriteWithCompute(source.c_str(), m_vbo, 0, GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT);
|
||||
};
|
||||
|
||||
// Round 1: the whole surface, (-1,-1) (1,-1) (-1,1) (1,1).
|
||||
writeQuad("0xBFF00000u, 0xBFF00000u, 0x3FF00000u, 0xBFF00000u, 0xBFF00000u, 0x3FF00000u, "
|
||||
"0x3FF00000u, 0x3FF00000u");
|
||||
Image image = Render(program, [] { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); });
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0)) && IsGreen(Half(image, 1)))
|
||||
<< "round 1: the quad is " << Half(image, 0) << " / " << Half(image, 1)
|
||||
<< " - black means the doubles were narrowed from the staged poison, not the written bytes";
|
||||
|
||||
// Round 2: only the left half, (-1,-1) (0,-1) (-1,1) (0,1).
|
||||
writeQuad("0xBFF00000u, 0xBFF00000u, 0u, 0xBFF00000u, 0xBFF00000u, 0x3FF00000u, 0u, 0x3FF00000u");
|
||||
image = Render(program, [] { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); });
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0))) << "round 2: the left half is " << Half(image, 0);
|
||||
EXPECT_FALSE(IsGreen(Half(image, 1)))
|
||||
<< "round 2: the right half still drew - the narrowed copy of round 1 was reused after the "
|
||||
"second GPU write";
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,102 @@
|
||||
# P8-C:Espryt server 暂存影子与 GPU 写
|
||||
|
||||
> 2026-09-29,分支 `p8/c`(基线 `8d3e2317`)。计划行:[`PLAN-P8.md`](PLAN-P8.md) 包 C。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
- 前提成立(red-once 见 §4):split 下 server 的 R-11 暂存影子只由上传 / adopt 填,server 上的 GPU 写不进影子;CPU 读者读到旧字节,或因无影子直接拒绝。原 7 例在基线上 Espryt split / spawn / tcp 与 inproc A/B 全红。
|
||||
- 修在 server,不动 client、不动 monolith:写者给 server 孪生体打标(`serverGpuWritten`,`MobileGL/MG_Backend/DirectGLES/Managers.h:1059`),读者消费字节前经 `SplitHostBytesForCpuRead`(`MobileGL/MG_Backend/DirectGLES/Managers.cpp:3570`)从 server 自己的 GL buffer 整 store 回读。
|
||||
- XFB 不打标:两条捕获路径本来就把字节拿在手里(给 client 的 writeback),直接写进影子(`StageServerWrittenRange`,`MobileGL/MG_Backend/DirectGLES/Managers.cpp:3626`)。
|
||||
- 懒刷新:只在读者真正消费字节时回读;原生 indirect 不从 CPU 取字段时不回读,Create / Flywheel 式每帧 compute 剔除 + 原生 indirect 不付回读(`MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:8555`)。
|
||||
- 顺带关掉 P3a M-3 声明的偏差(fp64 顶点收窄读旧字节)在 split 臂上的一半;monolith 臂不动。
|
||||
- monolith 两后端共 5 处缺陷,skip 按名、记给 dev(§6,ID-P8-3);IndirectCount 的 monolith 按 OQ15 不改。
|
||||
|
||||
## 2. 读者与写者
|
||||
|
||||
| 读者(split 臂) | 位置 | 做法 |
|
||||
|---|---|---|
|
||||
| primitive restart 重写 | `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:9309` | `SplitHostBytesForCpuRead`(整 store) |
|
||||
| multi-draw rebase | `MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp:196` | 同上 |
|
||||
| `*IndirectCount` 的 count | `DirectGLES.cpp:9632`、`:9798` | 参数 buffer 刷新;命令 buffer 仅在无影子时取第一份 |
|
||||
| indirect 命令字段 | `DirectGLES.cpp:8549` | CPU 路径全取;原生路径只在程序读 `gl_BaseVertex`、或无 `mg_IndirectParams` 视图而读 `gl_BaseInstance` 时取 |
|
||||
| 无影子的 indirect 命令 buffer | `DirectGLES.cpp:487` | 被 GPU 写过就取第一份(原来 decline,`indirect_command_bytes`) |
|
||||
| XFB scatter 的捕获前字节 | `DirectGLES.cpp:2037` | 刷新 |
|
||||
| fp64 顶点收窄 | `MobileGL/MG_Backend/DirectGLES/Managers.cpp:6671` | 刷新,并让 memo 失效(GPU 写不动 serial) |
|
||||
|
||||
| 写者 | 位置 | 效果 |
|
||||
|---|---|---|
|
||||
| SSBO 记录臂(draw 与 dispatch) | `DirectGLES.cpp:780` | 标记窗口内每个绑定点(与 client 的 `ShaderBufferEmit.h` 同口径) |
|
||||
| 原子计数记录臂 | `DirectGLES.cpp:1017` | 标记 |
|
||||
| 可写 buffer image | `DirectGLES.cpp:4464` | 纹理描述符的 `BufferForTexBuffer` → 标记 |
|
||||
| XFB 普通捕获 | `DirectGLES.cpp:1874` | 映射出的字节写进影子;T0 store 只标记(`:1829`) |
|
||||
| XFB scatter | `DirectGLES.cpp:2104` | 合成结果写进影子 |
|
||||
| 清标记 | `Managers.cpp:2308`(respecify)、刷新末尾 | — |
|
||||
|
||||
- 刷新顺序同 `Ops_H_Readback`:`FlushPendingRangesFrom` 先落排队上传 → `DrainResidentWritesNow` → 整 store `glMapBufferRange(READ)` → `MGL_SERVER_STAGED_ADOPT`(覆盖变整段,孤儿 store 也就有了第一份)→ serial 戳 + epoch bump。
|
||||
- T0 store:影子就是 server 对 client AHB 的一致映射;有标记或有排队常驻写时 `DrainResidentWritesNow` + `glFinish`(DEBTS 的 P11 B2 行的 split 半)。
|
||||
- 刷新与写影子只在复制型暂存 store(split)下生效(`CopiesIntoServerStorage`):monolith 的 store 保留调用方指针,映射一解就悬空。
|
||||
- 全部新代码在 `MOBILEGL_BUILD_DISAGGREGATED` 下;`ReadsBaseInstance`(`Managers.h:3107`)同。
|
||||
|
||||
## 3. 场景
|
||||
|
||||
`GpuWrittenDrawInputScenario`(`MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.cpp`,split-only 源,登记 `MobileGL/MG_IntegrationTest/CMakeLists.txt:3343`)12 例:GPU 只写一次,暂存影子里是画不出正确结果的毒值。
|
||||
|
||||
| 例 | 写者 | 读者 |
|
||||
|---|---|---|
|
||||
| RestartIndicesAComputeShaderWroteSplitTheStrip | SSBO | restart 重写 |
|
||||
| …IntoAnOrphanedStoreSplitTheStrip | SSBO,`glBufferData(NULL)` | restart 重写(原来无影子 → 跳过 draw) |
|
||||
| …SplitEveryMultiDrawStrip | SSBO | multi-draw rebase |
|
||||
| ACountAComputeShaderWroteDrivesMultiDrawElementsIndirectCount | SSBO(命令 + count) | count |
|
||||
| …DrivesMultiDrawArraysIndirectCount | SSBO | count |
|
||||
| ABaseVertexAComputeShaderWroteReachesGlBaseVertex | SSBO | 原生 indirect 的 `gl_BaseVertex` |
|
||||
| IndirectCommandsAComputeShaderWroteIntoAnOrphanedStoreDraw | SSBO,`glBufferData(NULL)` | 无影子命令 buffer |
|
||||
| RestartIndicesATransformFeedbackCapturedSplitTheStrip | XFB | restart 重写 |
|
||||
| RestartIndicesAScatteredTransformFeedbackCapturedSplitTheStrip | XFB + `gl_SkipComponents1`(scatter) | restart 重写 |
|
||||
| RestartIndicesImageStoresWroteSplitTheStrip | `imageStore` 到 buffer 纹理 | restart 重写 |
|
||||
| ACountAnAtomicCounterProducedDrivesMultiDrawElementsIndirectCount | 原子计数即参数 buffer | count |
|
||||
| DoubleVerticesAComputeShaderWroteAreNarrowedFromTheWrittenBytes | SSBO,两轮 | fp64 收窄(第 2 轮查 memo) |
|
||||
|
||||
- 臂:`DirectGLES.{Split,Spawn,Tcp}.`(门控);分裂构建里的 `DirectGLES.` / `DirectVulkan.` 即 monolith 对照与 inproc A/B;Magma split 各臂经 tier 2(`integration-magma-all-*`,信息层)与 `DirectVulkan.*.Full.`(`integration-magma-full-split`,门控)。
|
||||
- 三臂同源登记,`spawn_lane_parity.py` 无需例外行。
|
||||
|
||||
## 4. red-once
|
||||
|
||||
| # | 变异 | 载体 | 观察到的红 |
|
||||
|---|---|---|---|
|
||||
| 前提 | 基线 `8d3e2317` + 原 7 例 | `ctest -R GpuWrittenDrawInput`;inproc A/B 同命令加 gate 的环境 | Espryt split / spawn / tcp 21/21 红(像素:黑 = 旧毒值 / 跳过;baseVertex 例红色 = 几何对、`gl_BaseVertex` 旧);inproc A/B 7/7 红;Magma split 各臂全绿 |
|
||||
| F1 | 去 SSBO 标记、原子标记、image 标记、XFB 两处写影子(一个构建) | `ctest -R '^DirectGLES\.(Split|Spawn|Tcp)\.GpuWrittenDrawInput'` | 12/12 例在 Split、Spawn 红;Tcp 臂 Not Run(`TcpServer.Start` 撞上 :48440 的 TIME_WAIT),F2 覆盖了三臂;每例只依赖其中一个变异 |
|
||||
| F2 | 读者侧:restart / rebase / 两处 count 改回 `SplitHostBytes`,`CurrentIndirectCommandBytes` 恒返回原指针,去无影子取第一份,fp64 memo 去掉 `!sourceGpuWritten`(一个构建) | 同上 | 10 例 × 3 臂 = 30 红;两例 XFB 绿(它们的字节来自写影子,不经刷新,符合设计);fp64 例只红第 2 轮(memo) |
|
||||
| M | 把 `SkipBrokenMonolith` 探针成恒 false | `ctest -R '^Direct(GLES|Vulkan)\.GpuWrittenDrawInput'` | Espryt monolith:两例 count、原子 count、baseVertex、fp64 红;Magma monolith:6 例 restart SIGSEGV、Arrays count 红、fp64 SIGSEGV |
|
||||
|
||||
还原一律 copy + `cmp` + `touch`。
|
||||
|
||||
## 5. 门
|
||||
|
||||
- 场景:134 条(含 monolith skip 13 条)全绿;inproc A/B 24/24;相邻套件(Restart / MultiDraw / DrawParameters / IndexedDrawFamily / ClientVertexArray / Xfb / TransformFeedback / AtomicCounter / BufferTexture / DoublePrecision / ResidentIndex)1304/1304。
|
||||
- G1:`added=0 removed=0`,pull `.text` `a5ba13` 与基线相同。
|
||||
- retrace(主机):Espryt `SPLIT|SPAWN` 78 条 + `create-*` 12 条共 86 条,84 过;挂的 2 条是 `iris-iterationrp-in-world` 的宿主 llvmpipe JIT 崩溃(`LLVM ERROR: Cannot select ... vcvtps2ph`,已知项)。全量 236 条跑到 47 条(全过)时中止,全量留给集成。
|
||||
- create 的 SSIM:indirect Espryt 0.999961275 / Magma 0.999956667,instancing Espryt 0.999957572 / Magma 0.999952470,monolith、SPLIT、SPAWN 三臂逐位一致;Espryt indirect 与 P9 W2 普查的值相同([`../p9/W2-REMINT.md`](../p9/W2-REMINT.md) §4)。
|
||||
- 懒刷新实测:create-indirect / create-instancing 的 Espryt SPLIT、SPAWN 回读 0 次(临时把刷新日志提到 INFO,数 server 日志,未提交)。
|
||||
- 全门数字见包报告(`~/w7/notes/p8/c-report.md`)。
|
||||
|
||||
## 6. monolith 缺陷(给 dev,ID-P8-3)
|
||||
|
||||
| 后端 | 缺陷 | 位置 | 场景里的表现 |
|
||||
|---|---|---|---|
|
||||
| Espryt | `*IndirectCount` 读前端影子不调 `SyncGpuWrites`(OQ15) | `DirectGLES.cpp:9688-9689`、`:9852-9853` | count 读 0,什么都不画 |
|
||||
| Espryt | 原生 indirect 的 `gl_BaseVertex` 从前端影子取(单进程臂 `ResolveIndirectCommandBytes` 只 `SyncPersistentMappedRange`) | `DirectGLES.cpp:521`、`:8640` | 几何对、颜色红 |
|
||||
| Espryt | fp64 收窄的 handle 臂在 monolith 下读前端影子(P3a M-3 声明的偏差,monolith 半) | `Managers.cpp:6650` | 画在屏外 |
|
||||
| Magma | `MultiDrawArraysIndirectCount` 的 CPU 臂不调 `SyncGpuWrites` | `MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:729` | count 读 0 |
|
||||
| Magma | draw 录制中途调 `SyncGpuWrites`(restart 重写、GL_DOUBLE 顶点转换):提交并等待结束了当前帧命令缓冲,draw 接着往已结束的缓冲里录 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:4510`、`:4176` | SIGSEGV(lavapipe 工作线程 / `vkCmdBindVertexBuffers`);校验层 `VUID-vkCmdBindIndexBuffer-commandBuffer-recording` |
|
||||
|
||||
- skip 只在 `transportName == "monolith"` 的对应后端生效,理由串带出处。
|
||||
|
||||
## 7. 未做 / 已知边界
|
||||
|
||||
- T0 与常驻写那一半主机上无法 red(主机无 T0,split 下采纳档钉在 T2);代码按 `Ops_H_Readback` 的持久映射臂写,设备验证留给有设备的包。
|
||||
- 标记是过近似:窗口内每个绑定的 SSBO 每次 draw 都标记;留着 SSBO 绑定、又拿同一 buffer 做任意 restart 索引的 EBO 时,每 draw 回读一次。
|
||||
- bit 13 关(`BindingPointsComeFromRecords()` 为 false)的 A/B 配置下 SSBO / 原子不打标。
|
||||
- `Ops_H_Readback`(client 发起的回读)没有顺带刷新 server 影子(可省一次回读,未做)。
|
||||
- 同一 draw 既写 X(SSBO)又拿 X 当 EBO:标记在 `SyncNeccessaryBuffers` 里打,restart / rebase 读者在 draw 执行前就刷新并清标记,写发生在清之后,X 留成未标记。monolith 同形(`MarkShaderStorageBuffersGpuWritten` 先标,`SyncGpuWrites` 在 draw 前清),是对齐不是倒退。
|
||||
- DEBTS 的 P11 B2 行(handle 臂读采纳 / T0 映射前不落常驻写):split 半由 `SplitHostBytesForCpuRead` 的 T0 臂做了、主机不可验证;单进程半归 dev。
|
||||
- Magma 的 `VulkanRenderer.cpp:3683`(最大索引扫描)与 §6 最后一行同模式,未验证。
|
||||
Reference in New Issue
Block a user