From b8d10d0730884cc5383dac89a748dd155dadac9a Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 22 Sep 2026 20:07:25 -0400 Subject: [PATCH] [MG_Backend, tools] (P7): every WireDeclines.def row has a site and every site a warning-or-error line, and a tree-only audit fails the job when one does not The B3 review (ID-P7-34) counted 12 of the 52 rows with no site at all - TexViewFormat, TexNoApplierRecord, TexRecordDeadOrStale and the nine Shape* rows were declared and never counted - and ten sites that counted with no (MGLOG_D included) line, so "a name, a tally and a decline line" did not describe the tree. - SyncWireTextureShape: all nine Shape* exits now count; the ViewOf exit and the preserve-flush exit were bare `return false` and are now MGL_WIRE_DECLINE_AT; the image-flags exit logged only MGLOG_D, which a Release build compiles away, and is promoted likewise. - The preserve-copy exit was the one Shape exit with no row: appended as ShapePreserveCopyFailed (rows are append-only), 53 rows. - SyncTextureResourceByHandle / UploadPendingWireLevels: the count-only sites without a log of their own are promoted to MGL_WIRE_DECLINE_AT; the ones already under a W/E _ONCE line keep that line and gain the count. - scripts/ci/wire_declines_audit.py: fails on a row with no site, a site naming no row, or a bare Count() with no MGLOG_W/E within 8 lines above it. WireDeclineTally.h (the macro's definition) is not a site. Wired into the job that runs the fatal census. Audit before: 52 rows, 40 with sites, 10 unlogged under the W/E rule (rc 1). After: 53 rows, 53 with sites, 0 unlogged (rc 0). Forced red (ShapeNoVkFormat made true for 1x1 textures, reverted): Split.Fm.F1WireScenario.VertexIdSampler... fails on pixels with ShapeNoVkFormat=2 / TexShapeSync=2 in the shutdown tally. --- .github/workflows/test.yml | 5 + .../Renderer/VkTextureManager.cpp | 88 ++++++++++++--- .../DirectVulkan/Renderer/WireDeclines.def | 10 ++ scripts/ci/wire_declines_audit.py | 100 ++++++++++++++++++ 4 files changed, 187 insertions(+), 16 deletions(-) create mode 100755 scripts/ci/wire_declines_audit.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bee871ae..0bc7febd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2146,6 +2146,11 @@ jobs: - name: Fatal census (S7) run: python3 scripts/ci/fatal_census.py + # The decline half of rule I (P7 B3): every WireDeclines.def row has a site, every site a + # warning-or-error line. Same shape as the census - tree-only, no build. + - name: Wire declines audit (B3) + run: python3 scripts/ci/wire_declines_audit.py + - name: Load trace cases id: trace-cases run: | diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 09530959..2c0e73f7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -2071,13 +2071,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (desc.StorageKind != static_cast(TextureStorageType::Mipmap)) { MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: buffer-backed textures are not migrated; declined", desc.Resource.Slot, desc.Resource.Gen); + WireDeclineTally::Count(WireDeclineSite::ShapeBufferBacked); return false; } // ResolveWireTextureStorage has already followed ViewOf. Views share the storage // owner's image and layout; allocating a second image would break aliasing. - if (!MG_Pipe::MGPipeHandleIsNull(desc.ViewOf)) return false; + if (!MG_Pipe::MGPipeHandleIsNull(desc.ViewOf)) { + MGL_WIRE_DECLINE_AT(ShapeViewOfStorage, + "texture {slot=%u, gen=%u} is a view whose storage owner did not resolve", + desc.Resource.Slot, desc.Resource.Gen); + return false; + } if (desc.Levels == 0) { // A create before the first respecify: no storage, nothing to back. + MGL_WIRE_DECLINE_AT(ShapeNoLevels, "texture {slot=%u, gen=%u} has no defined level yet", + desc.Resource.Slot, desc.Resource.Gen); return false; } TextureShapeInfo shapeInfo{}; @@ -2086,6 +2094,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { "(%ux%ux%u, %u layers); declined", desc.Resource.Slot, desc.Resource.Gen, static_cast(desc.Target), desc.Width, desc.Height, desc.Depth, static_cast(desc.ArrayLayers)); + WireDeclineTally::Count(WireDeclineSite::ShapeNoVulkanShape); return false; } const TextureInternalFormat internalFormat = static_cast(desc.InternalFormat); @@ -2094,6 +2103,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (format == VK_FORMAT_UNDEFINED) { MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: no backing VkFormat for internal format 0x%x", desc.Resource.Slot, desc.Resource.Gen, desc.InternalFormat); + WireDeclineTally::Count(WireDeclineSite::ShapeNoVkFormat); return false; } // X8_D24 lacks optimal-tiling support on several drivers (lavapipe included); the same @@ -2116,6 +2126,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (!TryResolveSampleCountFlagBits(static_cast(desc.Samples), resolvedSampleCount)) { MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: unsupported sample count %u", desc.Resource.Slot, desc.Resource.Gen, static_cast(desc.Samples)); + WireDeclineTally::Count(WireDeclineSite::ShapeSampleCount); return false; } // glTexStorage*Multisample(samples = 1) is legal GL, but a one-sample image cannot @@ -2251,7 +2262,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { // old image must reach the queue first; waiting only for the preservation // fence cannot order work that is still in the renderer's open recording. // The descriptor caller preflights this before capturing its command buffer. - if (preserve && pVulkanRenderer && !pVulkanRenderer->FlushWirePendingCommandsForTextureUpdate()) return false; + if (preserve && pVulkanRenderer && !pVulkanRenderer->FlushWirePendingCommandsForTextureUpdate()) { + MGL_WIRE_DECLINE_AT(ShapePreserveFlushFailed, + "texture {slot=%u, gen=%u}: earlier work could not be submitted before " + "preserving its texels across a reallocation", + desc.Resource.Slot, desc.Resource.Gen); + return false; + } TextureResource replacement; VkImageCreateInfo imageInfo{}; @@ -2298,9 +2315,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { } if (imageFormatResult != VK_SUCCESS || (isMultisample && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) { - MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for texture {slot=%u, gen=%u}", - __func__, static_cast(imageInfo.flags), - static_cast(imageInfo.samples), desc.Resource.Slot, desc.Resource.Gen); + // Was an MGLOG_D, which a Release build compiles away: the one Shape* exit that + // left no line on the builds that ship. + MGL_WIRE_DECLINE_AT(ShapeImageFlagsUnsupported, + "texture {slot=%u, gen=%u}: image flags=0x%x sampleCount=%d are unsupported " + "by the device", + desc.Resource.Slot, desc.Resource.Gen, static_cast(imageInfo.flags), + static_cast(imageInfo.samples)); return false; } } @@ -2319,6 +2340,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { imageInfo.extent.height, imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels, static_cast(imageInfo.samples), static_cast(imageInfo.format)); + WireDeclineTally::Count(WireDeclineSite::ShapeCreateImageFailed); return false; } ++m_textureImageEpoch; // a new attachment image invalidates cached render passes @@ -2340,7 +2362,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (preserve) { FlushPendingUploads(); if (!PreserveTextureContentsOnRecreate(m_device, m_commandPool, m_graphicsQueue, - resource, replacement, true)) return false; + resource, replacement, true)) { + MGL_WIRE_DECLINE_AT(ShapePreserveCopyFailed, + "texture {slot=%u, gen=%u}: its texels could not be copied into the " + "reallocated image", + desc.Resource.Slot, desc.Resource.Gen); + return false; + } } DeferResourceRelease(Move(resource)); std::destroy_at(&resource); @@ -2412,11 +2440,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { // 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); + MGL_WIRE_DECLINE_AT(UploadLevelExtentMismatch, + "texture {slot=%u, gen=%u} level %u: the staged extent does not match the " + "image's extent for that level; left pending and NOT uploaded", + handle.Slot, handle.Gen, static_cast(level)); continue; } if (texelSize.x() <= 0 || texelSize.y() <= 0 || byteSize == 0) { - WireDeclineTally::Count(WireDeclineSite::UploadEmptyLevel); + MGL_WIRE_DECLINE_AT(UploadEmptyLevel, "texture {slot=%u, gen=%u} level %u is empty", + handle.Slot, handle.Gen, static_cast(level)); incomplete = true; continue; } @@ -2445,7 +2477,10 @@ 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); + MGL_WIRE_DECLINE_AT(UploadTexelArithmetic, + "texture {slot=%u, gen=%u} level %u: compressed, or its byte size is not a " + "whole number of texels", + handle.Slot, handle.Gen, static_cast(level)); incomplete = true; continue; } @@ -2465,7 +2500,10 @@ 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); + MGL_WIRE_DECLINE_AT(UploadRegionOutOfBounds, + "texture {slot=%u, gen=%u} level %u: a staged region lies outside " + "the level", + handle.Slot, handle.Gen, static_cast(level)); incomplete = true; return; } @@ -2521,7 +2559,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { WireDeclineTally::SetPendingGauge(static_cast(record.PendingUploads.size()), pendingBytes); } if (incomplete) { - WireDeclineTally::Count(WireDeclineSite::UploadIncomplete); + MGL_WIRE_DECLINE_AT(UploadIncomplete, + "texture {slot=%u, gen=%u}: at least one pending level could not be staged; " + "every draw that samples it is declined until it can", + handle.Slot, handle.Gen); return false; } if (items.empty()) { @@ -2533,7 +2574,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // 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()) { - WireDeclineTally::Count(WireDeclineSite::UploadFlushFailed); + MGL_WIRE_DECLINE_AT(UploadFlushFailed, + "texture {slot=%u, gen=%u}: earlier renderer work could not be submitted before " + "its upload batch", + handle.Slot, handle.Gen); return false; } @@ -2690,7 +2734,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkFormat viewFormat = VK_FORMAT_UNDEFINED; const auto storage = ResolveWireTextureStorage(handle, level, layer, &viewFormat); if (MG_Pipe::MGPipeHandleIsNull(storage)) { - WireDeclineTally::Count(WireDeclineSite::TexResolveStorage); + MGL_WIRE_DECLINE_AT(TexResolveStorage, + "texture {slot=%u, gen=%u}: no storage owner resolved", handle.Slot, + handle.Gen); return nullptr; } if (storage != handle) { @@ -2701,6 +2747,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { vkuFormatCompatibilityClass(viewFormat) != vkuFormatCompatibilityClass(resource->format))) { MGLOG_E_ONCE("Magma wire view: format %d cannot reinterpret storage format %d", static_cast(viewFormat), static_cast(resource->format)); + WireDeclineTally::Count(WireDeclineSite::TexViewFormat); return nullptr; } return resource; @@ -2711,6 +2758,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (handle.Slot >= records.size()) { MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: no applier record at that slot; declined", handle.Slot, handle.Gen); + WireDeclineTally::Count(WireDeclineSite::TexNoApplierRecord); return nullptr; } auto& record = records[handle.Slot]; @@ -2720,6 +2768,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: the applier's record is %s; declined", handle.Slot, handle.Gen, !record.Live ? "dead" : "of another generation"); + WireDeclineTally::Count(WireDeclineSite::TexRecordDeadOrStale); return nullptr; } const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); @@ -2731,15 +2780,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { return &resource; } if (!SyncWireTextureShape(record, resource, requireStorage)) { - WireDeclineTally::Count(WireDeclineSite::TexShapeSync); + MGL_WIRE_DECLINE_AT(TexShapeSync, "texture {slot=%u, gen=%u}: no backing image; see the Shape* tally", + handle.Slot, handle.Gen); return nullptr; } if (!renderbuffer && !UploadPendingWireLevels(handle, record, resource)) { - WireDeclineTally::Count(WireDeclineSite::TexUploadPendingLevels); + MGL_WIRE_DECLINE_AT(TexUploadPendingLevels, + "texture {slot=%u, gen=%u}: pending levels could not be uploaded; see the " + "Upload* tally", + handle.Slot, handle.Gen); return nullptr; } if (requireStorage && (resource.usageFlags & VK_IMAGE_USAGE_STORAGE_BIT) == 0) { - WireDeclineTally::Count(WireDeclineSite::TexStorageUsage); + MGL_WIRE_DECLINE_AT(TexStorageUsage, + "texture {slot=%u, gen=%u} is bound as a shader image but its allocation has no " + "STORAGE usage", + handle.Slot, handle.Gen); return nullptr; } resource.syncedWireSerial = record.Serial; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def index 31724f50..13b9e0b8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def @@ -20,6 +20,13 @@ // 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. // +// EVERY ROW HAS A SITE AND EVERY SITE HAS A LINE. A row with no site is a name that can never +// be counted - it reads as "this cannot happen here" when it only means "nobody wired it up" - +// and a site with no line puts the reader back on a bisect. Sites reached through a macro +// (MGL_WIRE_DECLINE_AT) get both at once; the few that use WireDeclineTally::Count directly do +// so ONLY because the statement above them already carries the same reason as an _ONCE log. +// scripts/ci/wire_declines_audit.py holds both halves. +// // MGL_WIRE_DECLINE(Name) // ---- SetupWireDraw, in statement order ------------------------------------------------- @@ -88,3 +95,6 @@ MGL_WIRE_DECLINE(ShapeSampleCount) MGL_WIRE_DECLINE(ShapePreserveFlushFailed) MGL_WIRE_DECLINE(ShapeImageFlagsUnsupported) MGL_WIRE_DECLINE(ShapeCreateImageFailed) +// Appended by the B3 review round: the preserve-copy arm of a reallocation was the one exit in +// SyncWireTextureShape still leaving no trace at all. +MGL_WIRE_DECLINE(ShapePreserveCopyFailed) diff --git a/scripts/ci/wire_declines_audit.py b/scripts/ci/wire_declines_audit.py new file mode 100755 index 00000000..a5efcfde --- /dev/null +++ b/scripts/ci/wire_declines_audit.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""P7 wave 2 package B3: WireDeclines.def rows and their sites must agree. + +CONTRACT-P7 ยง0 rule I admits two shapes for a wire-arm refusal, a decline and a named Fatal. +The decline half is only real if: + + (1) every row in WireDeclines.def is actually counted somewhere - a row with no site is a + name that can never move, which reads in a report as "this cannot happen here" when it + only means "nobody wired it up"; and + (2) every site emits a line - a counter with no log puts the next reader back on a bisect, + which is the thing the package exists to remove. + +Rows are APPEND-ONLY (the lanes assert specific counters), so (1) cannot be fixed later by +deleting a row once a release has shipped it. This check is why it cannot rot. + +Sites reached through MGL_WIRE_DECLINE_AT satisfy both at once. A bare +WireDeclineTally::Count(WireDeclineSite::X) is allowed ONLY when an MGLOG_ statement carrying +the same reason already stands just above it. + +usage: wire_declines_audit.py [repo-root] +""" +import os +import re +import sys + +RENDERER = "MobileGL/MG_Backend/DirectVulkan/Renderer" +DEF = os.path.join(RENDERER, "WireDeclines.def") +SOURCES = (".cpp", ".inc", ".h") +# How far above a bare Count() an MGLOG_ may stand and still be "the line for this site". +LOG_WINDOW = 8 + +ROW = re.compile(r"^\s*MGL_WIRE_DECLINE\(([A-Za-z][A-Za-z0-9]*)\)\s*$") +AT_SITE = re.compile(r"MGL_WIRE_DECLINE_AT\(\s*([A-Za-z][A-Za-z0-9]*)\s*,") +COUNT_SITE = re.compile(r"WireDeclineTally::Count\(\s*WireDeclineSite::([A-Za-z][A-Za-z0-9]*)\s*\)") +# Warning or error only: an MGLOG_D / MGLOG_V is compiled away in the Release builds that ship, +# so it is not a line anybody can read off a device. +LOGGED = re.compile(r"MGLOG_[WE](_ONCE)?\s*\(") + + +def main() -> int: + root = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else ".") + def_path = os.path.join(root, DEF) + if not os.path.isfile(def_path): + print("wire-declines: %s not found" % DEF, file=sys.stderr) + return 2 + + rows = [] + for line in open(def_path, encoding="utf-8"): + m = ROW.match(line) + if m: + rows.append(m.group(1)) + if len(rows) != len(set(rows)): + dupes = sorted({r for r in rows if rows.count(r) > 1}) + print("wire-declines: duplicate rows: %s" % ", ".join(dupes), file=sys.stderr) + return 1 + + counted = {} # name -> [(file, line)] + unlogged_counts = [] # (file, line, name) + src_dir = os.path.join(root, RENDERER) + for entry in sorted(os.listdir(src_dir)): + if not entry.endswith(SOURCES): + continue + # The header that DEFINES the macro and the enum names them all without being a site. + if entry == "WireDeclineTally.h": + continue + path = os.path.join(src_dir, entry) + lines = open(path, encoding="utf-8", errors="replace").read().splitlines() + for i, line in enumerate(lines): + for m in AT_SITE.finditer(line): + counted.setdefault(m.group(1), []).append((entry, i + 1)) + for m in COUNT_SITE.finditer(line): + name = m.group(1) + counted.setdefault(name, []).append((entry, i + 1)) + window = lines[max(0, i - LOG_WINDOW):i] + if not any(LOGGED.search(w) for w in window): + unlogged_counts.append((entry, i + 1, name)) + + orphan_rows = [r for r in rows if r not in counted] + unknown_sites = sorted(set(counted) - set(rows)) + + for name in orphan_rows: + print("wire-declines: row %s has NO SITE - it can never be counted" % name, file=sys.stderr) + for name in unknown_sites: + for where in counted[name]: + print("wire-declines: %s:%d counts %s, which is not a row in WireDeclines.def" + % (where[0], where[1], name), file=sys.stderr) + for entry, line, name in unlogged_counts: + print("wire-declines: %s:%d counts %s with no MGLOG_ within %d lines above it - a decline " + "must be readable from the log, not only from a counter" + % (entry, line, name, LOG_WINDOW), file=sys.stderr) + + bad = len(orphan_rows) + len(unknown_sites) + len(unlogged_counts) + print("wire-declines: %d rows, %d with sites, %d site(s) total, %d unlogged, %d unknown" + % (len(rows), len(rows) - len(orphan_rows), sum(len(v) for v in counted.values()), + len(unlogged_counts), len(unknown_sites))) + return 1 if bad else 0 + + +if __name__ == "__main__": + sys.exit(main())