From e52a997f3454719b8ace73f6c2dfbccdb62584a5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 29 Sep 2026 22:38:33 -0400 Subject: [PATCH 1/2] [Fix] (Espryt, P8-C): the split server refreshes its staged copy of a buffer a shader wrote before a CPU reader consumes it - storage/atomic/buffer-image writes mark the server twin, XFB captures land in the copy, the restart rewrite, multi-draw rebase, *IndirectCount, CPU-fed indirect fields, XFB scatter and fp64 narrowing read through SplitHostBytesForCpuRead; GpuWrittenDrawInputScenario (12 cases) on the split arms --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 145 +++- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 100 ++- MobileGL/MG_Backend/DirectGLES/Managers.h | 29 + MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp | 13 +- MobileGL/MG_IntegrationTest/CMakeLists.txt | 7 + .../Scenarios/GpuWrittenDrawInputScenario.cpp | 800 ++++++++++++++++++ 6 files changed, 1068 insertions(+), 26 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index b776965a..31e192ef 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -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(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(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; diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 0ff841b9..a140a656 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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 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(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 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 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(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(attrib.Offset) && stream.sourceStride == sourceStride && diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index bda68a71..fdca390d 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -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(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; diff --git a/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp b/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp index a9af52ea..0c07d93d 100644 --- a/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp +++ b/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp @@ -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"); diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 9929f305..5526bfd6 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -3340,6 +3340,13 @@ if (MOBILEGL_BUILD_DISAGGREGATED) TIMEOUT ${MGL_ITEST_TIMEOUT} ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_STRICT_ENVIRONMENT}" ) + # 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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.cpp new file mode 100644 index 00000000..cf818f85 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.cpp @@ -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 +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" +#include "../Harness/SplitRuntimePeek.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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 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 HalvesVertices() { + std::vector 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 + 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 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(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 vertices = HalvesVertices(); + BindVertices(vertices.data(), static_cast(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 vertices = HalvesVertices(); + BindVertices(vertices.data(), static_cast(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 vertices = HalvesVertices(); + BindVertices(vertices.data(), static_cast(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 vertices = HalvesVertices(); + BindVertices(vertices.data(), static_cast(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 vertices = HalvesVertices(); + BindVertices(vertices.data(), static_cast(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 From e815ce8d33e01d920642bf789bfb5e901d7bfce3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 29 Sep 2026 22:53:18 -0400 Subject: [PATCH 2/2] [Docs] (P8-C): the package note - readers and writers of the server's staged copy, the twelve GPU-written draw-input cases, each red-once, the retrace subset and the five monolith defects recorded for dev --- docs/Disaggregated/notes/p8/C.md | 102 +++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 docs/Disaggregated/notes/p8/C.md diff --git a/docs/Disaggregated/notes/p8/C.md b/docs/Disaggregated/notes/p8/C.md new file mode 100644 index 00000000..10afce0e --- /dev/null +++ b/docs/Disaggregated/notes/p8/C.md @@ -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 最后一行同模式,未验证。