From 76bbb6c3000ecdbec7c71fe865de316e9b33e554 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 22 Sep 2026 18:25:31 -0400 Subject: [PATCH] [MG_Backend] (P7): every silent exit on the Magma wire draw path has a name, a tally and a decline line Rule I admits two shapes for a refusal on the wire arm, a decline and a named Fatal. The draw path had a third one the rule does not allow: roughly forty bare `return false` statements with no log, no counter and no census entry, so a dropped draw left no trace anywhere - which is how the OpenRA device divergence survived four device windows with client and server logs that were line-identical between a passing and a failing run. WireDeclines.def names every one of them; WireDeclineTally.h expands that into a counter table plus MGL_WIRE_DECLINE_AT (the tally always moves, the log is once per site), in the shape of PipeApplier.h's ServerVerbSink tallies and for the same reason (R-16: a probe may not arm against a stub). Covered: SetupWireDraw and DispatchWireCompute (25), the three exits of PrepareWireTextureResources, the four of SyncTextureResourceByHandle, and UploadPendingWireLevels' five false exits PLUS the two that only `continue` - those report SUCCESS while leaving the entry pending, i.e. a draw that samples a level which was never uploaded, so they are counted apart. Also the pending-upload gauges (entries, bytes, peak bytes), sampled at the funnel, because a refused upload does not consume its entry and the level shadow behind it stays alive. Shutdown dumps the tally and is silent when it is all zero: a green lane stays quiet and a lane that dropped a draw cannot. Plus the drain probe behind MOBILEGL_MAGMA_WIREBUF_PROBE, which is what measured that a host replay reaches the mid-frame frame-boundary drain zero times. --- .../DirectVulkan/Renderer/UniformManager.cpp | 18 ++- .../Renderer/VkTextureManager.cpp | 48 +++++- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 22 +++ .../DirectVulkan/Renderer/WireDeclineTally.h | 141 ++++++++++++++++++ .../DirectVulkan/Renderer/WireDeclines.def | 90 +++++++++++ .../DirectVulkan/Renderer/WireDraw.inc | 117 ++++++++++++--- 6 files changed, 405 insertions(+), 31 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclineTally.h create mode 100644 MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index ef981b6e..f13551f3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -33,6 +33,8 @@ #include // P7 wave 0: the seam WireDescriptorFatal dies through. See MG_Pipe/PipeSessionFail.h. #include +// P7 wave 2 package B3: rule I's tally for this file's silent wire-draw exits. +#include "WireDeclineTally.h" #endif #include #include @@ -2944,7 +2946,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool UniformManager::PrepareWireTextureResources(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj) { if (!program.IsWire()) return true; - if (programObj.declinedDescriptors || m_textureManager == nullptr) return false; + if (programObj.declinedDescriptors || m_textureManager == nullptr) { + MGL_WIRE_DECLINE_AT(DescriptorDeclined, "the program carries a descriptor MobileGL could not resolve"); + return false; + } const auto& state = MG_Pipe::MGPipeApplier(); // Resolve writable images first: aliases sampled by the same draw must // see the final STORAGE-capable allocation before any view is built. @@ -2958,11 +2963,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { const Uint32 count = BindingDescriptorCount(programObj, binding); for (Uint32 element = 0; element < count; ++element) { const Int location = baseLocation + static_cast(element); - if (baseLocation < 0 || !program.UniformLocationsAliasSameUniform(baseLocation, location)) + if (baseLocation < 0 || !program.UniformLocationsAliasSameUniform(baseLocation, location)) { + MGL_WIRE_DECLINE_AT(SamplerLocationAlias, + "binding %u element %u does not alias one sampler uniform", binding, + element); return false; + } const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); if (unit < 0 || static_cast(unit) >= - (storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) return false; + (storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) { + MGL_WIRE_DECLINE_AT(SamplerUnitRange, "sampler/image unit %d is out of range", unit); + return false; + } const auto handle = storage ? state.BoundShaderImages[unit].Res : state.BoundSamplerViews[unit].Texture; // Null is the wire's unbound/incomplete binding. Its native diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 7e5acc0f..09530959 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -23,6 +23,8 @@ #include #include #include "../DirectVulkan.h" +// P7 wave 2 package B3: rule I's tally for the silent exits on this file's wire arm. +#include "WireDeclineTally.h" #endif #include #include @@ -2385,6 +2387,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: pending upload for level %u exceeds the " "image's %u levels; left pending", handle.Slot, handle.Gen, static_cast(level), resource.mipLevels); + WireDeclineTally::Count(WireDeclineSite::UploadLevelBeyondChain); continue; } const IntVec3 glExtent = store.LevelExtentOrUndefined(key, uploadTarget, level); @@ -2405,9 +2408,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { texelSize.y() != static_cast(std::max(resource.extent.height >> level, 1u)) || (volume && texelSize.z() != static_cast(std::max(resource.depth >> level, 1u))) || (!volume && Uint64(ResolveUploadArrayLayer(static_cast(uploadTarget))) + - static_cast(texelSize.z()) > resource.arrayLayers)) + static_cast(texelSize.z()) > resource.arrayLayers)) { + // NOT `incomplete`: this arm reports SUCCESS while leaving the entry pending, + // so the draw proceeds against texels that were never uploaded. Counted + // separately for exactly that reason (B3). + WireDeclineTally::Count(WireDeclineSite::UploadLevelExtentMismatch); continue; + } if (texelSize.x() <= 0 || texelSize.y() <= 0 || byteSize == 0) { + WireDeclineTally::Count(WireDeclineSite::UploadEmptyLevel); incomplete = true; continue; } @@ -2423,6 +2432,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { "level %u left pending", handle.Slot, handle.Gen, static_cast(resource.format), static_cast(level)); + WireDeclineTally::Count(WireDeclineSite::UploadConversion); incomplete = true; continue; } @@ -2435,6 +2445,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { (resource.aspect & VK_IMAGE_ASPECT_STENCIL_BIT); if (vkuFormatIsCompressed(resource.format) || !levelTexels || item.byteSize % levelTexels != 0 || item.byteSize / levelTexels == 0) { + WireDeclineTally::Count(WireDeclineSite::UploadTexelArithmetic); incomplete = true; continue; } @@ -2454,6 +2465,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (offset[axis] < 0 || extent[axis] <= 0 || static_cast(offset[axis]) + static_cast(extent[axis]) > static_cast(texelSize[axis])) { + WireDeclineTally::Count(WireDeclineSite::UploadRegionOutOfBounds); incomplete = true; return; } @@ -2496,7 +2508,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { addRegion({region.X, region.Y, region.Z, region.W, region.H, region.D}); } } - if (incomplete) return false; + // B3: the pending gauge is sampled here, on every pass through the funnel, because a + // refusal below does NOT consume the entries - they and their level shadows stay + // alive for the next draw to trip over (and, on a heavy-texture trace, to accumulate). + { + Uint64 pendingBytes = 0; + for (const auto& pending : record.PendingUploads) { + pendingBytes += static_cast( + store.LevelByteSize(MG_Remote::Server::StagedTextureStore::KeyForHandle(handle), + pending.UploadTarget, pending.Level)); + } + WireDeclineTally::SetPendingGauge(static_cast(record.PendingUploads.size()), pendingBytes); + } + if (incomplete) { + WireDeclineTally::Count(WireDeclineSite::UploadIncomplete); + return false; + } if (items.empty()) { return true; } @@ -2505,7 +2532,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Submit older renderer work before adding the new bytes to the batch, // otherwise Draw(old T), TexSubImage(T), Draw(new T) can upload before // the first draw. A clean texture never reaches this submission boundary. - if (pVulkanRenderer && !pVulkanRenderer->FlushWirePendingCommandsForTextureUpdate()) return false; + if (pVulkanRenderer && !pVulkanRenderer->FlushWirePendingCommandsForTextureUpdate()) { + WireDeclineTally::Count(WireDeclineSite::UploadFlushFailed); + return false; + } // UploadDirtyMipLevels' batching rules, verbatim: flush an open batch that already // writes this image and was touched by the open recording, and bound the staging bytes @@ -2659,7 +2689,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 level = 0, layer = 0; VkFormat viewFormat = VK_FORMAT_UNDEFINED; const auto storage = ResolveWireTextureStorage(handle, level, layer, &viewFormat); - if (MG_Pipe::MGPipeHandleIsNull(storage)) return nullptr; + if (MG_Pipe::MGPipeHandleIsNull(storage)) { + WireDeclineTally::Count(WireDeclineSite::TexResolveStorage); + return nullptr; + } if (storage != handle) { auto* resource = SyncTextureResourceByHandle(storage, false, requireStorage); if (!resource) return nullptr; @@ -2698,12 +2731,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { return &resource; } if (!SyncWireTextureShape(record, resource, requireStorage)) { + WireDeclineTally::Count(WireDeclineSite::TexShapeSync); return nullptr; } if (!renderbuffer && !UploadPendingWireLevels(handle, record, resource)) { + WireDeclineTally::Count(WireDeclineSite::TexUploadPendingLevels); + return nullptr; + } + if (requireStorage && (resource.usageFlags & VK_IMAGE_USAGE_STORAGE_BIT) == 0) { + WireDeclineTally::Count(WireDeclineSite::TexStorageUsage); return nullptr; } - if (requireStorage && (resource.usageFlags & VK_IMAGE_USAGE_STORAGE_BIT) == 0) return nullptr; resource.syncedWireSerial = record.Serial; return &resource; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index e40db172..ea2f2689 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -49,6 +49,8 @@ #include // P5c (G6): the named-blit arm's endpoint resolution runs inside the frontend-keyed scope. #include +// P7 wave 2 package B3: rule I's tally for WireDraw.inc's silent draw drops. +#include "WireDeclineTally.h" #endif #include #include @@ -3453,6 +3455,9 @@ void main() { VK_VERIFY(vkDeviceWaitIdle(m_device)); } #if MOBILEGL_BUILD_DISAGGREGATED + // P7 wave 2 package B3: the session's decline tally. Silent when nothing declined, so a + // green lane stays quiet and a lane that dropped a draw cannot (rule I's observable). + WireDeclineTally::Dump("shutdown"); DestroyWireDrawPass(); CollectWireObjects(m_submitCounter, true); ClearAllWireDrawPassCaches(); @@ -13688,6 +13693,23 @@ void main() { // loops still rewind the arena and age their caches every 8 iterations - // bounded by 8 iterations' transient usage. ++m_drainsSinceLastPresent; +#if MOBILEGL_BUILD_DISAGGREGATED + // B3 probe: a drain that reaches here found the GPU caught up and nothing recording. + // Every 8th one is treated as a FRAME BOUNDARY - mid-frame. See the note. + { + static const Bool probe = [] { + const char* value = std::getenv("MOBILEGL_MAGMA_WIREBUF_PROBE"); + return value && value[0] == '1'; + }(); + if (probe) { + MGLOG_I("WBUF drain#%llu%s submit=%llu completed=%llu", + static_cast(m_drainsSinceLastPresent), + (m_drainsSinceLastPresent % 8) == 0 ? " ***FRAME-BOUNDARY-WORK***" : "", + static_cast(m_submitCounter), + static_cast(m_completedSubmitCounter)); + } + } +#endif if ((m_drainsSinceLastPresent % 8) != 0) { return true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclineTally.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclineTally.h new file mode 100644 index 00000000..11098717 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclineTally.h @@ -0,0 +1,141 @@ +// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclineTally.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once + +#include +#if MOBILEGL_BUILD_DISAGGREGATED + +#include +#include + +#include + +namespace MobileGL::MG_Backend::DirectVulkan { + +// P7 wave 2 package B3: the per-site tally behind CONTRACT-P7 §0 rule I. +// +// Shape is PipeApplier.h's ServerVerbSink tallies, and for the same reason (R-16: a probe may +// not arm against a stub). "The retrace passed" and "the retrace silently dropped nine draws" +// produce the same exit code and the same log, so the lane has to assert the number that only +// this path can move. +// +// Counters are process-wide and monotone. On the spawn arm they live in the SERVER process, +// which is where the draw path runs - a client-side reader would always see zero, so the +// two-process case asserts pixels and reads these from the server's own dump. +enum class WireDeclineSite : Uint32 { +#define MGL_WIRE_DECLINE(name) name, +#include "WireDeclines.def" +#undef MGL_WIRE_DECLINE + Count +}; + +inline const char* WireDeclineSiteName(WireDeclineSite site) { + switch (site) { +#define MGL_WIRE_DECLINE(name) \ + case WireDeclineSite::name: \ + return #name; +#include "WireDeclines.def" +#undef MGL_WIRE_DECLINE + case WireDeclineSite::Count: + break; + } + return "Unnamed"; +} + +class WireDeclineTally { +public: + static constexpr SizeT kSiteCount = static_cast(WireDeclineSite::Count); + + static void Count(WireDeclineSite site) { + Slots()[static_cast(site)].fetch_add(1, std::memory_order_relaxed); + Total().fetch_add(1, std::memory_order_relaxed); + } + + static Uint64 Get(WireDeclineSite site) { + return Slots()[static_cast(site)].load(std::memory_order_relaxed); + } + + static Uint64 TotalDeclines() { return Total().load(std::memory_order_relaxed); } + + // The memory half (package M1's thread): a refused upload does not consume its + // PendingUpload entry, so the entry and the level shadow behind it stay alive. These are + // GAUGES, not counters - the last observed live depth, sampled where the refusal happens - + // so a monotone climb across frames is the signature that a refusal is also a leak. + static void SetPendingGauge(Uint64 entries, Uint64 bytes) { + PendingEntries().store(entries, std::memory_order_relaxed); + PendingBytes().store(bytes, std::memory_order_relaxed); + Uint64 peak = PeakPendingBytes().load(std::memory_order_relaxed); + while (bytes > peak && !PeakPendingBytes().compare_exchange_weak(peak, bytes, std::memory_order_relaxed)) { + } + } + static Uint64 PendingUploadEntries() { return PendingEntries().load(std::memory_order_relaxed); } + static Uint64 PendingUploadBytes() { return PendingBytes().load(std::memory_order_relaxed); } + static Uint64 PeakPendingUploadBytes() { return PeakPendingBytes().load(std::memory_order_relaxed); } + + static void Reset() { + for (SizeT i = 0; i < kSiteCount; ++i) Slots()[i].store(0, std::memory_order_relaxed); + Total().store(0, std::memory_order_relaxed); + PendingEntries().store(0, std::memory_order_relaxed); + PendingBytes().store(0, std::memory_order_relaxed); + PeakPendingBytes().store(0, std::memory_order_relaxed); + } + + // One line per site that moved, plus the pending gauges. Called at session teardown and + // from the forced-condition harness; silent when nothing declined, so a green lane stays + // quiet and a lane that dropped a draw cannot. + static void Dump(const char* tag) { + if (TotalDeclines() == 0 && PeakPendingUploadBytes() == 0) return; + MGLOG_I("MGWIRE-DECLINES[%s] total=%llu pendingEntries=%llu pendingBytes=%llu peakPendingBytes=%llu", tag, + static_cast(TotalDeclines()), + static_cast(PendingUploadEntries()), + static_cast(PendingUploadBytes()), + static_cast(PeakPendingUploadBytes())); + for (SizeT i = 0; i < kSiteCount; ++i) { + const Uint64 value = Slots()[i].load(std::memory_order_relaxed); + if (!value) continue; + MGLOG_I("MGWIRE-DECLINES[%s] %s=%llu", tag, WireDeclineSiteName(static_cast(i)), + static_cast(value)); + } + } + +private: + static std::atomic* Slots() { + static std::atomic slots[kSiteCount]{}; + return slots; + } + static std::atomic& Total() { + static std::atomic total{0}; + return total; + } + static std::atomic& PendingEntries() { + static std::atomic value{0}; + return value; + } + static std::atomic& PendingBytes() { + static std::atomic value{0}; + return value; + } + static std::atomic& PeakPendingBytes() { + static std::atomic value{0}; + return value; + } +}; + +// Every decline goes through this: the tally moves ALWAYS, the log line is once per site. +// `return MGL_WIRE_DECLINE_FALSE(Site, "why", ...)` reads as the statement it replaces. +#define MGL_WIRE_DECLINE_AT(site, fmt, ...) \ + do { \ + ::MobileGL::MG_Backend::DirectVulkan::WireDeclineTally::Count( \ + ::MobileGL::MG_Backend::DirectVulkan::WireDeclineSite::site); \ + MGLOG_E_ONCE("Magma wire draw declined [" #site "]: " fmt, ##__VA_ARGS__); \ + } while (0) + +} // namespace MobileGL::MG_Backend::DirectVulkan + +#endif // MOBILEGL_BUILD_DISAGGREGATED diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def new file mode 100644 index 00000000..31724f50 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def @@ -0,0 +1,90 @@ +// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def +// 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 + +// P7 wave 2 package B3. THE NAMES OF EVERY WAY A MAGMA WIRE DRAW CAN DECLINE. +// +// CONTRACT-P7 §0 rule I says a refusal on the wire arm is either a named decline or a named +// Fatal. Before B3 the draw path had a third shape the rule does not admit: a bare +// `return false` with no log, no counter and no census entry - roughly twenty of them in +// SetupWireDraw alone, plus the texture-sync chain behind PrepareWireTextureResources. A draw +// that took one of those exits left NO trace anywhere, which is precisely why the OpenRA +// device divergence survived four device windows with client and server logs that were +// LINE-IDENTICAL between a passing and a failing run. +// +// Each row below is one such exit. The name is what the decline logs and what the tally +// counts, so "which exit did this frame take" is answerable from a counter dump instead of +// from a bisect. Rows are append-only: the integration lane asserts specific counters moved. +// +// MGL_WIRE_DECLINE(Name) + +// ---- SetupWireDraw, in statement order ------------------------------------------------- +MGL_WIRE_DECLINE(ProgramLinkOrSpirv) +MGL_WIRE_DECLINE(PointSizeCapability) +MGL_WIRE_DECLINE(TextureResourcesUnprepared) +MGL_WIRE_DECLINE(VertexFormatUnsupported) +MGL_WIRE_DECLINE(VertexAttributeWindow) +MGL_WIRE_DECLINE(VertexStreamCountZero) +MGL_WIRE_DECLINE(VertexStreamRead) +MGL_WIRE_DECLINE(VertexStreamConverter) +MGL_WIRE_DECLINE(VertexTransientUpload) +MGL_WIRE_DECLINE(IndexViewMissing) +MGL_WIRE_DECLINE(IndexTypeWidth) +MGL_WIRE_DECLINE(IndexWindow) +MGL_WIRE_DECLINE(IndexStreamRead) +MGL_WIRE_DECLINE(IndexTransientUpload) +MGL_WIRE_DECLINE(IndexSliceInvalid) +MGL_WIRE_DECLINE(UniformBufferBinding) +MGL_WIRE_DECLINE(NoAttachments) +MGL_WIRE_DECLINE(PipelineUnavailable) +MGL_WIRE_DECLINE(CurrentAttributePayload) +MGL_WIRE_DECLINE(CurrentAttributeUpload) +MGL_WIRE_DECLINE(BeginRenderPass) + +// ---- DispatchWireCompute ---------------------------------------------------------------- +MGL_WIRE_DECLINE(ComputeProgramLinkOrSpirv) +MGL_WIRE_DECLINE(ComputeTextureResourcesUnprepared) +MGL_WIRE_DECLINE(ComputePipelineUnavailable) +MGL_WIRE_DECLINE(ComputeUniformBufferBinding) + +// ---- UniformManager::PrepareWireTextureResources ---------------------------------------- +MGL_WIRE_DECLINE(DescriptorDeclined) +MGL_WIRE_DECLINE(SamplerLocationAlias) +MGL_WIRE_DECLINE(SamplerUnitRange) + +// ---- VkTextureManager::SyncTextureResourceByHandle -------------------------------------- +MGL_WIRE_DECLINE(TexResolveStorage) +MGL_WIRE_DECLINE(TexViewFormat) +MGL_WIRE_DECLINE(TexNoApplierRecord) +MGL_WIRE_DECLINE(TexRecordDeadOrStale) +MGL_WIRE_DECLINE(TexShapeSync) +MGL_WIRE_DECLINE(TexUploadPendingLevels) +MGL_WIRE_DECLINE(TexStorageUsage) + +// ---- VkTextureManager::UploadPendingWireLevels ------------------------------------------ +// The five `incomplete`/false exits the investigation named, plus the two that leave an +// entry pending WITHOUT failing (a `continue`) - those are the ones that can starve a draw +// of its texels while still reporting success, so they are counted separately. +MGL_WIRE_DECLINE(UploadLevelBeyondChain) +MGL_WIRE_DECLINE(UploadLevelExtentMismatch) +MGL_WIRE_DECLINE(UploadEmptyLevel) +MGL_WIRE_DECLINE(UploadConversion) +MGL_WIRE_DECLINE(UploadTexelArithmetic) +MGL_WIRE_DECLINE(UploadRegionOutOfBounds) +MGL_WIRE_DECLINE(UploadIncomplete) +MGL_WIRE_DECLINE(UploadFlushFailed) + +// ---- VkTextureManager::SyncWireTextureShape --------------------------------------------- +MGL_WIRE_DECLINE(ShapeBufferBacked) +MGL_WIRE_DECLINE(ShapeViewOfStorage) +MGL_WIRE_DECLINE(ShapeNoLevels) +MGL_WIRE_DECLINE(ShapeNoVulkanShape) +MGL_WIRE_DECLINE(ShapeNoVkFormat) +MGL_WIRE_DECLINE(ShapeSampleCount) +MGL_WIRE_DECLINE(ShapePreserveFlushFailed) +MGL_WIRE_DECLINE(ShapeImageFlagsUnsupported) +MGL_WIRE_DECLINE(ShapeCreateImageFailed) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index cb33ac78..fd1cf1ec 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -203,7 +203,10 @@ Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* indices) { auto& state = MG_Pipe::MGPipeApplier(); const auto program = WireProgramSource(state.BoundShaderCso); - if (!program.GetLinkStatus() || !program.GetSpirvStatus()) return false; + if (!program.GetLinkStatus() || !program.GetSpirvStatus()) { + MGL_WIRE_DECLINE_AT(ProgramLinkOrSpirv, "program is not linked or carries no SPIR-V"); + return false; + } if (aspects & DrawSetupAspect::IndirectDrawBuffer) WireBufferLegacyFatal(); MakeXfbWritesVisible(); @@ -224,10 +227,16 @@ flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex; programObject = &m_programFactory->GetOrCreateProgram(program, flags); } - if (programObject->pointSizeCapabilityUnsupported) return false; + if (programObject->pointSizeCapabilityUnsupported) { + MGL_WIRE_DECLINE_AT(PointSizeCapability, "the program writes gl_PointSize and the device cannot express it"); + return false; + } const auto& programObj = *programObject; m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined; - if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) return false; + if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) { + MGL_WIRE_DECLINE_AT(TextureResourcesUnprepared, "a sampled/storage image could not be made ready; see the Tex* tally"); + return false; + } // Lazy image uploads/promotions can submit the old recording. Resolve // ALL attachments before descriptor binding captures the current cmd. for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { @@ -277,6 +286,7 @@ "mask 0x%x whose enabled array has no supported vertex format", program.GetExternalIndex(), input.unsupportedAttribMask & programObj.activeVertexInputLocationMask); + WireDeclineTally::Count(WireDeclineSite::VertexFormatUnsupported); return false; } Vector vertexSlices(input.bindings.size()); @@ -291,7 +301,10 @@ const VkDeviceSize offset = input.bindingBaseOffsets[binding]; const auto type = static_cast(attr.Type); const SizeT elementSize = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra); - if (offset > slice.size || elementSize > slice.size - offset) return false; + if (offset > slice.size || elementSize > slice.size - offset) { + MGL_WIRE_DECLINE_AT(VertexAttributeWindow, "an enabled attribute's first element lies outside its buffer"); + return false; + } const auto conversion = input.bindingConversions[binding]; if (conversion != VertexInputStateFactory::VertexStreamConversion::None) { const SizeT stride = static_cast(attr.Stride); @@ -300,9 +313,15 @@ // name arbitrary vertices, so preserve the entire valid source stream. if (stride && !(aspects & DrawSetupAspect::IndexBuffer) && !resource.Divisor) count = std::min(count, static_cast(drawParams.firstVertex) + drawParams.vertexCount); - if (!count) return false; + if (!count) { + MGL_WIRE_DECLINE_AT(VertexStreamCountZero, "the converted vertex stream would hold no elements"); + return false; + } Vector source((count - 1) * stride + elementSize); - if (!m_bufferManager.ReadWireBuffer(resource.Res, offset, source.size(), source.data())) return false; + if (!m_bufferManager.ReadWireBuffer(resource.Res, offset, source.size(), source.data())) { + MGL_WIRE_DECLINE_AT(VertexStreamRead, "the vertex bytes to convert could not be read back"); + return false; + } Vector converted; // P7 wave 2 package C, CONTRACT-P7 §3.2 `vertex-format-conversion`: DECLINE. // The converter is asked only for a conversion BuildWireVertexInput already @@ -318,10 +337,14 @@ "location=%u type=%u size=%u conversion=%d", location, attr.Type, static_cast(attr.Size), static_cast(conversion)); + WireDeclineTally::Count(WireDeclineSite::VertexStreamConverter); return false; } if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), - converted.data(), converted.size(), 16, vertexSlices[binding])) return false; + converted.data(), converted.size(), 16, vertexSlices[binding])) { + MGL_WIRE_DECLINE_AT(VertexTransientUpload, "the converted vertex stream did not fit the transient arena"); + return false; + } } else { slice.offset += offset; slice.size -= offset; @@ -332,9 +355,15 @@ BufferSlice indexSlice{}; VkIndexType indexType = VK_INDEX_TYPE_MAX_ENUM; if (aspects & DrawSetupAspect::IndexBuffer) { - if (!indices) return false; + if (!indices) { + MGL_WIRE_DECLINE_AT(IndexViewMissing, "an indexed draw arrived with no index view"); + return false; + } const SizeT width = MG_Util::GetGLTypeSize(indices->indexType); - if (width != 1 && width != 2 && width != 4) return false; + if (width != 1 && width != 2 && width != 4) { + MGL_WIRE_DECLINE_AT(IndexTypeWidth, "the index type is not 1, 2 or 4 bytes wide"); + return false; + } indexType = width == 4 ? VK_INDEX_TYPE_UINT32 : width == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT8; const auto& restart = MGB_CTX->GetRenderStateParameters(); const Uint32 fixed = MG_Util::FixedRestartIndexForGLType(indices->indexType); @@ -350,10 +379,16 @@ MagmaWireFatal("index-buffer-resource"); const Uint64 offset = state.IndexBuffer.Offset + indices->indexByteOffset; if (offset < state.IndexBuffer.Offset || offset > indexSlice.size || - indices->indexByteSize > indexSlice.size - offset) return false; + indices->indexByteSize > indexSlice.size - offset) { + MGL_WIRE_DECLINE_AT(IndexWindow, "the index range lies outside its buffer"); + return false; + } if (replaceRestart || expandByte || offset % width) { copied.resize(indices->indexByteSize); - if (!m_bufferManager.ReadWireBuffer(state.IndexBuffer.Res, offset, copied.size(), copied.data())) return false; + if (!m_bufferManager.ReadWireBuffer(state.IndexBuffer.Res, offset, copied.size(), copied.data())) { + MGL_WIRE_DECLINE_AT(IndexStreamRead, "the indices to rewrite could not be read back"); + return false; + } bytes = copied.data(); } indexSlice.offset += offset; @@ -377,9 +412,15 @@ if (expandByte) indexType = VK_INDEX_TYPE_UINT16; } if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), - bytes, uploadSize, 16, indexSlice)) return false; + bytes, uploadSize, 16, indexSlice)) { + MGL_WIRE_DECLINE_AT(IndexTransientUpload, "the rewritten indices did not fit the transient arena"); + return false; + } + } + if (!indexSlice.IsValid()) { + MGL_WIRE_DECLINE_AT(IndexSliceInvalid, "no usable index slice was resolved"); + return false; } - if (!indexSlice.IsValid()) return false; } if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); @@ -387,7 +428,10 @@ // Resolve it before constructing framebuffer views so both roles of that image // name the same allocation even on its first storage-image use. if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj, - m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS)) return false; + m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS)) { + MGL_WIRE_DECLINE_AT(UniformBufferBinding, "the program's uniform/storage bindings could not be bound"); + return false; + } Vector attachments; Vector viewInfos; @@ -459,7 +503,10 @@ depth.attachment = add(fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT); else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone) depth.attachment = add(fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT); - if (attachments.empty()) return false; + if (attachments.empty()) { + MGL_WIRE_DECLINE_AT(NoAttachments, "the draw framebuffer resolved to no attachments"); + return false; + } passKey.width = extent.width; passKey.height = extent.height; passKey.layers = layers; @@ -516,7 +563,10 @@ const Uint64 compatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey); const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass, ResolvePrimitiveRestartEnable(aspects, indices), compatibilityId); - if (pipeline == VK_NULL_HANDLE) return false; + if (pipeline == VK_NULL_HANDLE) { + MGL_WIRE_DECLINE_AT(PipelineUnavailable, "no graphics pipeline could be created for this draw"); + return false; + } for (Uint32 i = 0; i < vertexSlices.size(); ++i) vkCmdBindVertexBuffers(frame.commandBuffer, i, 1, &vertexSlices[i].buffer, &vertexSlices[i].offset); Uint32 binding = static_cast(vertexSlices.size()); @@ -527,14 +577,23 @@ const void* data = nullptr; VkDeviceSize size = 0; if (!TryGetCurrentVertexAttributeUploadPayload(MGB_CTX->GetCurrentVertexAttribute(location), - programObj.vertexInputTypes[location], format, data, size)) return false; + programObj.vertexInputTypes[location], format, data, size)) { + MGL_WIRE_DECLINE_AT(CurrentAttributePayload, "a disabled array's current attribute value has no upload payload"); + return false; + } BufferSlice slice{}; if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), - data, size, 16, slice)) return false; + data, size, 16, slice)) { + MGL_WIRE_DECLINE_AT(CurrentAttributeUpload, "the current attribute value did not fit the transient arena"); + return false; + } vkCmdBindVertexBuffers(frame.commandBuffer, binding++, 1, &slice.buffer, &slice.offset); } if (indexSlice.IsValid()) vkCmdBindIndexBuffer(frame.commandBuffer, indexSlice.buffer, indexSlice.offset, indexType); - if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) return false; + if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) { + MGL_WIRE_DECLINE_AT(BeginRenderPass, "the render pass for this draw could not be begun"); + return false; + } vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); ApplyDynamicDrawStateTail(frame, pass.extent, fbo->IsDefault, ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin)); @@ -546,14 +605,26 @@ RetireWireDrawPass(); auto& frame = m_frameContext.GetCurrent(); const auto program = WireProgramSource(MG_Pipe::MGPipeApplier().DispatchProgram); - if (!program.GetLinkStatus() || !program.GetSpirvStatus()) return; + if (!program.GetLinkStatus() || !program.GetSpirvStatus()) { + MGL_WIRE_DECLINE_AT(ComputeProgramLinkOrSpirv, "the compute program is not linked or carries no SPIR-V"); + return; + } const auto& programObj = m_programFactory->GetOrCreateProgram(program, {}); - if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) return; + if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) { + MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch"); + return; + } const auto pipeline = GetOrCreateComputePipeline(programObj); - if (!pipeline) return; + if (!pipeline) { + MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch"); + return; + } if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj, - m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) return; + m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) { + MGL_WIRE_DECLINE_AT(ComputeUniformBufferBinding, "the dispatch's uniform/storage bindings could not be bound"); + return; + } vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); vkCmdDispatch(frame.commandBuffer, x, y, z); }