[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.
This commit is contained in:
2026-09-22 18:44:07 -04:00
parent b089e0de9b
commit 76bbb6c300
6 changed files with 405 additions and 31 deletions
@@ -33,6 +33,8 @@
#include <MG_Pipe/PipeApply.h>
// P7 wave 0: the seam WireDescriptorFatal dies through. See MG_Pipe/PipeSessionFail.h.
#include <MG_Pipe/PipeSessionFail.h>
// P7 wave 2 package B3: rule I's tally for this file's silent wire-draw exits.
#include "WireDeclineTally.h"
#endif
#include <vulkan/utility/vk_format_utils.h>
#include <algorithm>
@@ -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<Int>(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<Uint>(location));
if (unit < 0 || static_cast<Uint32>(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
@@ -23,6 +23,8 @@
#include <MG_Pipe/PipeApply.h>
#include <MG_Remote/Server/StagedTextureStore.h>
#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 <algorithm>
#include <cstdio>
@@ -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<Uint32>(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<Int>(std::max(resource.extent.height >> level, 1u)) ||
(volume && texelSize.z() != static_cast<Int>(std::max(resource.depth >> level, 1u))) ||
(!volume && Uint64(ResolveUploadArrayLayer(static_cast<TextureUploadTarget>(uploadTarget))) +
static_cast<Uint64>(texelSize.z()) > resource.arrayLayers))
static_cast<Uint64>(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<Int>(resource.format),
static_cast<Uint32>(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<Uint64>(offset[axis]) + static_cast<Uint64>(extent[axis]) >
static_cast<Uint64>(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<Uint64>(
store.LevelByteSize(MG_Remote::Server::StagedTextureStore::KeyForHandle(handle),
pending.UploadTarget, pending.Level));
}
WireDeclineTally::SetPendingGauge(static_cast<Uint64>(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;
}
@@ -49,6 +49,8 @@
#include <MG_Remote/Server/ServerLoop.h>
// P5c (G6): the named-blit arm's endpoint resolution runs inside the frontend-keyed scope.
#include <MG_Impl/Pipe/SlotAllocator.h>
// P7 wave 2 package B3: rule I's tally for WireDraw.inc's silent draw drops.
#include "WireDeclineTally.h"
#endif
#include <algorithm>
#include <bit>
@@ -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<unsigned long long>(m_drainsSinceLastPresent),
(m_drainsSinceLastPresent % 8) == 0 ? " ***FRAME-BOUNDARY-WORK***" : "",
static_cast<unsigned long long>(m_submitCounter),
static_cast<unsigned long long>(m_completedSubmitCounter));
}
}
#endif
if ((m_drainsSinceLastPresent % 8) != 0) {
return true;
}
@@ -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 <Config.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Includes.h>
#include <MG_Util/Debug/Log.h>
#include <atomic>
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<SizeT>(WireDeclineSite::Count);
static void Count(WireDeclineSite site) {
Slots()[static_cast<SizeT>(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<SizeT>(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<unsigned long long>(TotalDeclines()),
static_cast<unsigned long long>(PendingUploadEntries()),
static_cast<unsigned long long>(PendingUploadBytes()),
static_cast<unsigned long long>(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<WireDeclineSite>(i)),
static_cast<unsigned long long>(value));
}
}
private:
static std::atomic<Uint64>* Slots() {
static std::atomic<Uint64> slots[kSiteCount]{};
return slots;
}
static std::atomic<Uint64>& Total() {
static std::atomic<Uint64> total{0};
return total;
}
static std::atomic<Uint64>& PendingEntries() {
static std::atomic<Uint64> value{0};
return value;
}
static std::atomic<Uint64>& PendingBytes() {
static std::atomic<Uint64> value{0};
return value;
}
static std::atomic<Uint64>& PeakPendingBytes() {
static std::atomic<Uint64> 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
@@ -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)
@@ -203,7 +203,10 @@
Flags<DrawSetupAspect> 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<BufferSlice> vertexSlices(input.bindings.size());
@@ -291,7 +301,10 @@
const VkDeviceSize offset = input.bindingBaseOffsets[binding];
const auto type = static_cast<DataType>(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<SizeT>(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<SizeT>(count, static_cast<SizeT>(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<Uint8> 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<Uint8> 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<Uint32>(attr.Size),
static_cast<Int>(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<VkAttachmentDescription> attachments;
Vector<VkImageViewCreateInfo> 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<Uint32>(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);
}