diff --git a/CMakeLists.txt b/CMakeLists.txt index c8c41dbf..98d887b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -655,6 +655,11 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam). MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp + # P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve + # arms and the probe that measures the first against the second, shared by the + # renderer's bring-up and the Vulkan POST. Wire-only - the monolith records neither arm - + # so it stays out of the pull build (gate G1). + MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp ) endif() diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index aa74065f..0c15fa71 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -53,6 +53,7 @@ // P7 wave 2 package B3: rule I's tally for WireDraw.inc's silent draw drops. #include "WireDeclineTally.h" #include "WireDepthResolveArm.h" +#include "WireDepthResolveProbe.h" #endif #include #include @@ -15148,28 +15149,9 @@ void main() { if (m_wireShaderStencilExport) EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions, VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); - // P7 gate 5 (g5-msrbo): the Adreno's no-draw depth/stencil resolve pass writes nothing, - // so the shader resolve goes first there (WireDepthResolveArm.h says why and where). - // - // THE VENDOR THE POLICY READS CAN BE SUBSTITUTED, AND ONLY HERE (review round). No host - // lane runs on a Qualcomm device, so the order this line picks for the Redmi ran nowhere - // a lane could see it: dropping this assignment, or the `m_wirePreferShaderDepthResolve ||` - // in ResolveWireDepthStencil, left every lane green and put - // KHR-GL46.direct_state_access.renderbuffers_storage_multisample back at 54 Fail lines on - // the device. MGITEST_MAGMA_DEPTH_RESOLVE_VENDOR_ID (strtoul base 0, so `0x5143`) replaces - // the id handed to WirePrefersShaderDepthResolve and nothing else - no other vendor - // check reads it - and the DirectVulkan.{Split,Spawn}.MsResolveQ./MsFlipQ. entries set it - // to Qualcomm's id and read ResolveWireDepthStencil's arm line back from the server log. - Uint32 depthResolvePolicyVendor = m_physicalDevice.properties.vendorID; - if (const char* substitute = std::getenv("MGITEST_MAGMA_DEPTH_RESOLVE_VENDOR_ID"); - substitute != nullptr && *substitute != '\0') - depthResolvePolicyVendor = static_cast(std::strtoul(substitute, nullptr, 0)); - m_wirePreferShaderDepthResolve = MG_Config::Transport != MG_Config::TransportMode::Monolith && - WirePrefersShaderDepthResolve(depthResolvePolicyVendor); - if (m_wirePreferShaderDepthResolve) - MGLOG_I("DirectVulkan: vendor 0x%x (policy vendor 0x%x) - the wire arm resolves multisample " - "depth/stencil with the shader pass first (the no-draw resolve render pass is the fallback)", - m_physicalDevice.properties.vendorID, depthResolvePolicyVendor); + // P7 gate 5 (g5-msprobe): which multisample depth/stencil resolve arm goes first is measured + // once the device exists (ArmWireDepthResolveOrder, below), not keyed on the vendor. + m_wirePreferShaderDepthResolve = false; #endif if ((descriptorIndexingCore || descriptorIndexingExtension) && getPhysicalDeviceFeatures2 != nullptr && getPhysicalDeviceProperties2 != nullptr) { @@ -15826,8 +15808,81 @@ void main() { // Last, because it records on m_graphicsQueue: decide the PRIMITIVES_GENERATED // reroute for XFB-inactive draws. Nothing else has touched the queue yet. ArmPrimGenReroute(); +#if MOBILEGL_BUILD_DISAGGREGATED + // Same terms: it records on m_graphicsQueue, which nothing but the probe above has used. + ArmWireDepthResolveOrder(); +#endif } +#if MOBILEGL_BUILD_DISAGGREGATED + // P7 gate 5 (g5-msprobe): THE ARM ORDER OF THE MULTISAMPLE DEPTH/STENCIL RESOLVE IS A MEASURED + // PROPERTY OF THIS DEVICE. WireDepthResolveArm.h says why (the Adreno 830's no-draw + // VK_KHR_depth_stencil_resolve pass writes nothing), WireDepthResolveProbe.h what is run. The + // answer is a device property, so it is memoized per process like the PRIMITIVES_GENERATED + // probe's, and logged once with every reading - the line the device evidence quotes. + // + // THE MEASUREMENT CAN BE REPLACED, AND ONLY HERE: MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=bug|clean + // hands ChooseWireDepthResolveArm a canned measurement instead of running the probe - no host + // lane runs on a device with the defect - and the DirectVulkan.{Split,Spawn}.MsResolveBug./ + // MsFlipBug. entries force `bug` and read ResolveWireDepthStencil's arm line back from the + // server log. The canned measurement is evaluated like a real one, so those entries go red + // when the evaluation stops detecting the defect. + void VulkanRenderer::ArmWireDepthResolveOrder() { + m_wirePreferShaderDepthResolve = false; + if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; + // ResolveWireDepthStencil's own test for the render-pass arm, less its per-format half + // (the probe asks that per format): without it the shader pass is the only arm. + const Bool renderPassArmAvailable = !MagmaWireForcedShaderDepthResolve() && m_wireCreateRenderPass2 && + (m_wireDepthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT) != 0; + static const WireDepthResolveArmChoice s_choice = [&]() { + const char* knobValue = std::getenv("MGITEST_MAGMA_DEPTH_RESOLVE_PROBE"); + const WireDepthResolveProbeKnob knob = ParseWireDepthResolveProbeKnob(knobValue); + if (knob == WireDepthResolveProbeKnob::Unrecognised) + MGLOG_W("DirectVulkan: MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=%s is neither `bug` nor `clean`; " + "the resolve probe runs as if it were unset", knobValue); + WireDepthResolveArmChoice choice = ChooseWireDepthResolveArm(knob, renderPassArmAvailable, [&]() { + WireDepthResolveProbeContext context; + context.physicalDevice = m_physicalDevice.handle; + context.device = m_device; + context.queue = m_graphicsQueue; + context.queueFamilyIndex = static_cast(m_physicalDevice.queueFamilies.graphicsFamily); + context.createRenderPass2 = m_wireCreateRenderPass2; + context.depthResolveModes = m_wireDepthResolveModes; + context.stencilResolveModes = m_wireStencilResolveModes; + context.shaderStencilExport = m_wireShaderStencilExport; + return RunWireDepthResolveProbe(context); + }); + if (choice.measurement.fenceWaitTimedOut) { + // Its objects are leaked on purpose (see the PRIMITIVES_GENERATED probe above for + // why this device must not be idle-waited on their account). + MGLOG_W("DirectVulkan: the depth/stencil resolve probe timed out waiting on its own " + "submission; its Vulkan objects are deliberately leaked and the render-pass arm " + "keeps its place"); + } else if (choice.verdict == WireDepthResolveProbeVerdict::Inconclusive) { + MGLOG_W("DirectVulkan: depth/stencil resolve probe verdict=inconclusive - the shader " + "control did not resolve the probe's inputs either, so nothing is concluded about " + "the render pass and the default arm order stands"); + } + MGLOG_I("DirectVulkan: depth/stencil resolve probe (%s) verdict=%s: %s%s%s", + choice.measurement.fromKnob ? "forced by MGITEST_MAGMA_DEPTH_RESOLVE_PROBE" + : renderPassArmAvailable ? "measured on this device" + : "not run: no render-pass arm", + WireDepthResolveProbeVerdictName(choice.verdict), + choice.preferShader ? "the shader pass resolves first, the no-draw render pass is the fallback" + : (renderPassArmAvailable || choice.measurement.fromKnob) + ? "the VK_KHR_depth_stencil_resolve render pass resolves first" + : "the shader pass is the only arm", + choice.measurement.failureReason.empty() ? "" : "; ", + choice.measurement.failureReason.c_str()); + for (const WireDepthResolveFormatReading& reading : choice.measurement.formats) + MGLOG_I("DirectVulkan: depth/stencil resolve probe %s", + DescribeWireDepthResolveFormat(reading).c_str()); + return choice; + }(); + m_wirePreferShaderDepthResolve = s_choice.preferShader; + } +#endif + void VulkanRenderer::ArmPrimGenReroute() { using namespace MG_Util::SelfTest; m_primGenRerouteKind = PrimGenRerouteKind::None; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 66e56f08..55b9d52f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -556,9 +556,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { WireMultisampleResolveResources* m_wireMultisampleResolveResources = nullptr; void DestroyWireMultisampleResolveResources(); Bool m_wireShaderStencilExport = false; - // WirePrefersShaderDepthResolve(vendorID) for this device (WireDepthResolveArm.h): the - // shader arm resolves first, the render-pass arm is the fallback (P7 gate 5). + // The resolve probe's verdict for this device (WireDepthResolveArm.h, + // WireDepthResolveProbe.h): true when the no-draw render pass was measured leaving its + // target unwritten while the shader control resolved - the shader arm then resolves + // first and the render-pass arm is the fallback (P7 gate 5, g5-msprobe). Bool m_wirePreferShaderDepthResolve = false; + // Runs the probe (memoized per process) and sets the member above. Called at the end of + // device creation, after ArmPrimGenReroute: it records on m_graphicsQueue. + void ArmWireDepthResolveOrder(); Bool BlitWireColorToDefault(WireImage source, WireImage destination, GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveArm.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveArm.h index eec0604c..bf7d9500 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveArm.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveArm.h @@ -9,7 +9,7 @@ #include -// P7 gate 5 (g5-msrbo): WHICH ARM RESOLVES A MULTISAMPLE DEPTH/STENCIL ASPECT FIRST. +// P7 gate 5 (g5-msrbo, g5-msprobe): WHICH ARM RESOLVES A MULTISAMPLE DEPTH/STENCIL ASPECT FIRST. // // The wire arm has two (WireFramebuffer.inc, ResolveWireDepthStencil): a render pass that carries // a VK_KHR_depth_stencil_resolve attachment and no draw, and the baked shader pass of @@ -18,17 +18,222 @@ // its resolve target unwritten - KHR-GL46.direct_state_access.renderbuffers_storage_multisample // read every resolved depth and stencil value back as 0 on the inproc arm while the monolith arm // passed, and the same case passed on the device with the shader arm forced -// (MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=1). So on a Qualcomm device the shader arm goes first; -// the render pass stays the fallback for what the shader cannot write (a stencil aspect without -// VK_EXT_shader_stencil_export). Every other vendor keeps B2's order: nothing measured says -// otherwise, and a vendor list that grew on a guess would be the same mistake the other way round. +// (MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=1). // -// A pure function of the vendor id so the policy is a unit-testable fact rather than a branch -// buried in the apply thread. +// A DRIVER THAT CLAIMS WHAT IT CANNOT DO IS A POST PROBE, NOT A VENDOR QUIRK (standing rule, +// 2026-08-22). The first fix keyed the order on Qualcomm's vendor id; it is replaced by a probe +// that reproduces the production resolve on the actual VkDevice (WireDepthResolveProbe.cpp) and +// asks the one question the order depends on: does the no-draw render pass write the resolve +// target? A driver that fixes the defect gets the fast arm back, and a driver of any vendor that +// has it gets the shader arm - neither needs a table edit. +// +// EVERY BUG PROBE CARRIES A CONTROL (MG_Util/SelfTest/DriverBugProbes.h). The control is the +// production SHADER arm fed the same inputs: when it does not resolve them either, the probe has +// measured its own setup (or a device that cannot resolve depth at all) rather than the render +// pass, the verdict is INCONCLUSIVE, and the default order stands. +// +// This header is the pure half - the measurement, the verdict, the test knob and the choice - so +// a unit test pins every mapping with a fake result source (MG_Test/Pipeline/PipelineQuirkTest.cpp). +// The Vulkan half that fills the measurement is WireDepthResolveProbe.{h,cpp}. namespace MobileGL::MG_Backend::DirectVulkan { - inline constexpr Uint32 kWireVendorIdQualcomm = 0x5143u; + // One aspect of one probed format, both arms. Values are the RAW aspect words the + // buffer copy returns (D16: 16 bits; D24: the low 24 bits of the 32-bit word; D32F: the float's + // bits; stencil: 8 bits), compared within `tolerance` (1 LSB for a UNORM depth, 0 otherwise). + struct WireDepthResolveAspectReading { + // The format has this aspect and the render-pass arm's copy of it was read back. + Bool measured = false; + Uint32 texels = 0; + Uint32 expected = 0; // the multisample source's cleared value + Uint32 sentinel = 0; // what the resolve target was pre-filled with + Uint32 tolerance = 0; + // THE SUBJECT: VK_KHR_depth_stencil_resolve render pass, no draw. + Uint32 renderPassMatches = 0; // texels within tolerance of `expected` + Uint32 renderPassSentinels = 0; // texels still holding the sentinel + Uint32 renderPassFirst = 0; // texel (0,0) + // THE CONTROL: the production shader pass, same inputs. False where it cannot run for this + // aspect (a stencil aspect without VK_EXT_shader_stencil_export, a failed pipeline). + Bool shaderRan = false; + Uint32 shaderMatches = 0; + Uint32 shaderFirst = 0; + }; - inline constexpr Bool WirePrefersShaderDepthResolve(Uint32 vendorId) { - return vendorId == kWireVendorIdQualcomm; + struct WireDepthResolveFormatReading { + String name; // e.g. "D24_UNORM_S8_UINT" + Int32 format = 0; // the VkFormat + Uint32 samples = 0; + // False when the format was not probed; `skipReason` says why (unsupported, the render-pass + // arm would not be taken for it, an object the probe needed could not be created). + Bool ran = false; + String skipReason; + WireDepthResolveAspectReading depth, stencil; + }; + + struct WireDepthResolveProbeMeasurement { + // At least one format was recorded, submitted and read back. + Bool ran = false; + // The bounded fence wait expired: every object the probe created was deliberately leaked + // (the queue may still be executing it), and nothing was read back. + Bool fenceWaitTimedOut = false; + String failureReason; + // Came from MGITEST_MAGMA_DEPTH_RESOLVE_PROBE, not from the device. + Bool fromKnob = false; + Vector formats; + }; + + enum class WireDepthResolveProbeVerdict : Uint8 { + // Nothing was measured: the render-pass arm is not available (nothing to choose between), + // or the probe could not set itself up. The default order stands. + NotRun, + // Measured, but no format's CONTROL resolved: the probe cannot tell the render pass's + // defect from its own. The default order stands, and the report must never call it a bug. + Inconclusive, + // Every format whose control resolved was resolved by the render pass too. + Clean, + // The defect: on at least one format the shader control resolved the inputs and the + // render pass did not (wrote nothing - the sentinel survived - or wrote wrong values). + RenderPassResolveBroken, + }; + + inline const char* WireDepthResolveProbeVerdictName(WireDepthResolveProbeVerdict verdict) { + switch (verdict) { + case WireDepthResolveProbeVerdict::NotRun: return "not-run"; + case WireDepthResolveProbeVerdict::Inconclusive: return "inconclusive"; + case WireDepthResolveProbeVerdict::Clean: return "clean"; + case WireDepthResolveProbeVerdict::RenderPassResolveBroken: return "render-pass-resolve-broken"; + } + return "?"; + } + + inline Bool WireDepthResolveControlHolds(const WireDepthResolveAspectReading& aspect) { + return aspect.measured && aspect.shaderRan && aspect.texels > 0 && aspect.shaderMatches == aspect.texels; + } + + inline Bool WireDepthResolveSubjectHolds(const WireDepthResolveAspectReading& aspect) { + return aspect.texels > 0 && aspect.renderPassMatches == aspect.texels; + } + + // An aspect only counts where its control holds; a format is conclusive when any of its + // aspects is. The render pass resolves every aspect of the format in one pass, so one aspect + // it fails is the defect for the whole arm. + inline WireDepthResolveProbeVerdict EvaluateWireDepthResolveProbe(const WireDepthResolveProbeMeasurement& measurement) { + if (!measurement.ran || measurement.fenceWaitTimedOut) return WireDepthResolveProbeVerdict::NotRun; + Bool conclusive = false, broken = false; + for (const WireDepthResolveFormatReading& reading : measurement.formats) { + if (!reading.ran) continue; + for (const WireDepthResolveAspectReading* aspect : {&reading.depth, &reading.stencil}) { + if (!WireDepthResolveControlHolds(*aspect)) continue; + conclusive = true; + if (!WireDepthResolveSubjectHolds(*aspect)) broken = true; + } + } + if (broken) return WireDepthResolveProbeVerdict::RenderPassResolveBroken; + return conclusive ? WireDepthResolveProbeVerdict::Clean : WireDepthResolveProbeVerdict::Inconclusive; + } + + // THE TEST KNOB, read by the SERVER (split and spawn arms only; the tcp server is a lane fixture + // whose environment no entry reaches - spawn_lane_parity.py's MAGMA_SERVER_ENV_KNOB_NO_TCP). + // Lavapipe resolves correctly through both arms, so no host lane can reach the verdict that + // matters on the Redmi. MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=bug|clean replaces the MEASUREMENT + // with a canned one that says exactly that, and nothing else: the canned measurement still goes + // through EvaluateWireDepthResolveProbe and the same member, so an entry that forces `bug` is red + // when the evaluation stops detecting the defect. + enum class WireDepthResolveProbeKnob : Uint8 { Measure, ForceBug, ForceClean, Unrecognised }; + + inline WireDepthResolveProbeKnob ParseWireDepthResolveProbeKnob(const char* value) { + if (value == nullptr || *value == '\0') return WireDepthResolveProbeKnob::Measure; + if (std::strcmp(value, "bug") == 0) return WireDepthResolveProbeKnob::ForceBug; + if (std::strcmp(value, "clean") == 0) return WireDepthResolveProbeKnob::ForceClean; + return WireDepthResolveProbeKnob::Unrecognised; + } + + // One packed format's worth of readings: the control resolved both aspects; the render pass + // either did too (`renderPassWrites`) or left the sentinel everywhere. + inline WireDepthResolveProbeMeasurement CannedWireDepthResolveProbeMeasurement(Bool renderPassWrites) { + WireDepthResolveProbeMeasurement measurement; + measurement.ran = true; + measurement.fromKnob = true; + WireDepthResolveFormatReading reading; + reading.name = "MGITEST_MAGMA_DEPTH_RESOLVE_PROBE"; + reading.samples = 4; + reading.ran = true; + const auto fill = [&](WireDepthResolveAspectReading& aspect, Uint32 expected, Uint32 sentinel) { + aspect.measured = true; + aspect.texels = 16; + aspect.expected = expected; + aspect.sentinel = sentinel; + aspect.renderPassMatches = renderPassWrites ? 16 : 0; + aspect.renderPassSentinels = renderPassWrites ? 0 : 16; + aspect.renderPassFirst = renderPassWrites ? expected : sentinel; + aspect.shaderRan = true; + aspect.shaderMatches = 16; + aspect.shaderFirst = expected; + }; + fill(reading.depth, 0x400000u, 0xBFFFFFu); + fill(reading.stencil, 0x5Au, 0xA5u); + measurement.formats.push_back(Move(reading)); + return measurement; + } + + struct WireDepthResolveArmChoice { + WireDepthResolveProbeMeasurement measurement; + WireDepthResolveProbeVerdict verdict = WireDepthResolveProbeVerdict::NotRun; + // m_wirePreferShaderDepthResolve: the shader pass goes first, the render pass is the + // fallback for what the shader cannot write. + Bool preferShader = false; + }; + + // The whole decision, with the Vulkan half behind `measure` (called at most once, and only when + // the knob does not decide and the render-pass arm exists at all: without it the shader pass + // is the only arm and there is no order to choose). + template + WireDepthResolveArmChoice ChooseWireDepthResolveArm(WireDepthResolveProbeKnob knob, Bool renderPassArmAvailable, + MeasureFn&& measure) { + WireDepthResolveArmChoice choice; + if (knob == WireDepthResolveProbeKnob::ForceBug || knob == WireDepthResolveProbeKnob::ForceClean) { + choice.measurement = CannedWireDepthResolveProbeMeasurement(knob == WireDepthResolveProbeKnob::ForceClean); + } else if (renderPassArmAvailable) { + choice.measurement = measure(); + } + choice.verdict = EvaluateWireDepthResolveProbe(choice.measurement); + choice.preferShader = choice.verdict == WireDepthResolveProbeVerdict::RenderPassResolveBroken; + return choice; + } + + // One format's readings, both arms: "D24_UNORM_S8_UINT x4: depth expected 0x400000 sentinel + // 0xbfffff: render pass 0/16 (first 0xbfffff, sentinel 16), shader control 16/16 (first + // 0x400000) / stencil ...". + inline String DescribeWireDepthResolveFormat(const WireDepthResolveFormatReading& reading) { + const auto aspectText = [](const char* name, const WireDepthResolveAspectReading& aspect) { + String text = format("{} expected 0x{:x} sentinel 0x{:x}: render pass {}/{} (first 0x{:x}, sentinel {}), ", + name, aspect.expected, aspect.sentinel, aspect.renderPassMatches, aspect.texels, + aspect.renderPassFirst, aspect.renderPassSentinels); + if (aspect.shaderRan) { + text += format("shader control {}/{} (first 0x{:x})", aspect.shaderMatches, aspect.texels, + aspect.shaderFirst); + } else { + text += "no shader control"; + } + return text; + }; + String out = reading.name; + if (!reading.ran) return out + " skipped (" + reading.skipReason + ")"; + out += format(" x{}: ", reading.samples); + if (reading.depth.measured) out += aspectText("depth", reading.depth); + if (reading.depth.measured && reading.stencil.measured) out += " / "; + if (reading.stencil.measured) out += aspectText("stencil", reading.stencil); + return out; + } + + // Every format, "; "-separated, for the POST row. + inline String DescribeWireDepthResolveProbe(const WireDepthResolveProbeMeasurement& measurement) { + if (!measurement.ran) { + return measurement.failureReason.empty() ? String("not run") : "not run (" + measurement.failureReason + ")"; + } + String out; + for (const WireDepthResolveFormatReading& reading : measurement.formats) { + if (!out.empty()) out += "; "; + out += DescribeWireDepthResolveFormat(reading); + } + return out; } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp new file mode 100644 index 00000000..e4eb48b2 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp @@ -0,0 +1,987 @@ +// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp +// Copyright (c) 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 +// +// See WireDepthResolveProbe.h for the arms and the probe's shape, WireDepthResolveArm.h for the +// verdict. The plumbing is the PRIMITIVES_GENERATED probe's (MG_Util/SelfTest/ +// PrimitivesGeneratedNoXfbProbe.cpp): one throwaway command buffer, a bounded fence wait that +// leaks rather than idle-waits a hung GPU, teardown on every other path. + +#include "WireDepthResolveProbe.h" + +#include + +#include +#include +#include +#include + +#if MOBILEGL_BUILD_DISAGGREGATED +namespace MobileGL::MG_Backend::DirectVulkan { +#include "WireMultisampleResolveSpirv.h" + + VkImageAspectFlags WireDepthStencilFormatAspects(VkFormat format) { + if (format == VK_FORMAT_S8_UINT) return VK_IMAGE_ASPECT_STENCIL_BIT; + if (format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT || + format == VK_FORMAT_D32_SFLOAT_S8_UINT) + return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + return VK_IMAGE_ASPECT_DEPTH_BIT; + } + + VkImageViewCreateInfo WireDepthStencilViewInfo(VkImage image, VkFormat format, VkImageAspectFlags aspects, + Uint32 level, Uint32 layer) { + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.image = image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = format; + info.subresourceRange = {aspects, level, 1, layer, 1}; + return info; + } + + VkResult CreateWireDepthStencilResolvePass(PFN_vkCreateRenderPass2 createRenderPass2, VkDevice device, + VkFormat format, VkSampleCountFlagBits samples, VkRenderPass* pass) { + const VkImageAspectFlags formatAspects = WireDepthStencilFormatAspects(format); + VkAttachmentDescription2 attachments[2]{}; + for (Uint32 i = 0; i < 2; ++i) { + auto& attachment = attachments[i]; + attachment.sType = VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2; + attachment.format = format; + attachment.samples = i == 0 ? samples : VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = attachment.stencilLoadOp = + i == 0 ? VK_ATTACHMENT_LOAD_OP_LOAD : VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + } + VkAttachmentReference2 sourceReference{VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2}; + sourceReference.attachment = 0; + sourceReference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + sourceReference.aspectMask = formatAspects; + VkAttachmentReference2 resolveReference = sourceReference; + resolveReference.attachment = 1; + VkSubpassDescriptionDepthStencilResolve resolve{VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_DEPTH_STENCIL_RESOLVE}; + resolve.depthResolveMode = + (formatAspects & VK_IMAGE_ASPECT_DEPTH_BIT) ? VK_RESOLVE_MODE_SAMPLE_ZERO_BIT : VK_RESOLVE_MODE_NONE; + resolve.stencilResolveMode = + (formatAspects & VK_IMAGE_ASPECT_STENCIL_BIT) ? VK_RESOLVE_MODE_SAMPLE_ZERO_BIT : VK_RESOLVE_MODE_NONE; + resolve.pDepthStencilResolveAttachment = &resolveReference; + VkSubpassDescription2 subpass{VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_2}; + subpass.pNext = &resolve; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &sourceReference; + VkRenderPassCreateInfo2 passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2}; + passInfo.attachmentCount = 2; + passInfo.pAttachments = attachments; + passInfo.subpassCount = 1; + passInfo.pSubpasses = &subpass; + return createRenderPass2(device, &passInfo, nullptr, pass); + } + + VkResult CreateWireShaderResolveKit(VkDevice device, Bool shaderStencilExport, WireShaderResolveKit& kit) { + const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = bytes; + info.pCode = code; + return vkCreateShaderModule(device, &info, nullptr, &module); + }; + VkResult result = + shader(kWireMultisampleResolveVertexSpirv, sizeof(kWireMultisampleResolveVertexSpirv), kit.vertex); + if (result != VK_SUCCESS) return result; + result = shader(kWireMultisampleDepthResolveFragmentSpirv, sizeof(kWireMultisampleDepthResolveFragmentSpirv), + kit.depthFragment); + if (result != VK_SUCCESS) return result; + // THE STENCIL MODULE IS ONLY CREATED WHERE IT CAN RUN: its SPIR-V declares the + // StencilExportEXT capability, and handing a module with an unsupported capability to + // vkCreateShaderModule is undefined behaviour - not an error this could catch. + if (shaderStencilExport) { + result = shader(kWireMultisampleStencilResolveFragmentSpirv, + sizeof(kWireMultisampleStencilResolveFragmentSpirv), kit.stencilFragment); + if (result != VK_SUCCESS) return result; + } + const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, + VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; + VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + layout.bindingCount = 1; + layout.pBindings = &binding; + result = vkCreateDescriptorSetLayout(device, &layout, nullptr, &kit.descriptorLayout); + if (result != VK_SUCCESS) return result; + // NO PUSH CONSTANT RANGE AT ALL: the pass reads its coordinate from gl_FragCoord and its + // rectangle from the scissor, so neither stage takes an input. + VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + pipelineLayout.setLayoutCount = 1; + pipelineLayout.pSetLayouts = &kit.descriptorLayout; + result = vkCreatePipelineLayout(device, &pipelineLayout, nullptr, &kit.pipelineLayout); + if (result != VK_SUCCESS) return result; + // NEAREST: texelFetch ignores the sampler's filter, and most devices do not advertise + // linear filtering for a depth format anyway. The sampler exists only because GLSL's + // sampler2DMS is a COMBINED image sampler. + VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST; + sampler.maxLod = 0.0f; + return vkCreateSampler(device, &sampler, nullptr, &kit.nearest); + } + + void DestroyWireShaderResolveKit(VkDevice device, WireShaderResolveKit& kit) { + vkDestroySampler(device, kit.nearest, nullptr); + vkDestroyPipelineLayout(device, kit.pipelineLayout, nullptr); + vkDestroyDescriptorSetLayout(device, kit.descriptorLayout, nullptr); + vkDestroyShaderModule(device, kit.vertex, nullptr); + vkDestroyShaderModule(device, kit.depthFragment, nullptr); + vkDestroyShaderModule(device, kit.stencilFragment, nullptr); + kit = {}; + } + + VkResult CreateWireShaderResolveRenderPass(VkDevice device, VkFormat format, VkRenderPass* pass) { + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + // DONT_CARE on BOTH aspects: the pass writes every texel of the scissored rect for the + // aspect it owns, and the caller copies only that aspect out. The other aspect of a packed + // format is left undefined on purpose - it is never read, and LOADing it would order this + // pass behind a write nobody is waiting for. + attachment.loadOp = attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + const VkAttachmentReference depthStencil{0, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &depthStencil; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = 1; + info.pAttachments = &attachment; + info.subpassCount = 1; + info.pSubpasses = &subpass; + return vkCreateRenderPass(device, &info, nullptr, pass); + } + + VkResult CreateWireShaderResolvePipeline(VkDevice device, const WireShaderResolveKit& kit, VkRenderPass pass, + VkImageAspectFlags aspect, VkPipeline* pipeline) { + const VkShaderModule fragment = + aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? kit.stencilFragment : kit.depthFragment; + if (fragment == VK_NULL_HANDLE || kit.vertex == VK_NULL_HANDLE) return VK_ERROR_FEATURE_NOT_PRESENT; + VkPipelineShaderStageCreateInfo stages[2]{}; + for (auto& stage : stages) { + stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stage.pName = "main"; + } + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = kit.vertex; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport.viewportCount = viewport.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + raster.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineDepthStencilStateCreateInfo depthStencilState{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthStencilState.minDepthBounds = 0.0f; + depthStencilState.maxDepthBounds = 1.0f; + if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) { + // ALWAYS + write: gl_FragDepth is the answer, and a comparison against whatever + // DONT_CARE left behind would discard some of it. + depthStencilState.depthTestEnable = VK_TRUE; + depthStencilState.depthWriteEnable = VK_TRUE; + depthStencilState.depthCompareOp = VK_COMPARE_OP_ALWAYS; + } else { + // REPLACE on every outcome, compare ALWAYS, full write mask: that is what makes the + // shader-exported reference the value that lands. The static `reference` is overridden + // by gl_FragStencilRefARB, which is the whole point of the extension. + depthStencilState.stencilTestEnable = VK_TRUE; + VkStencilOpState op{}; + op.failOp = op.passOp = op.depthFailOp = VK_STENCIL_OP_REPLACE; + op.compareOp = VK_COMPARE_OP_ALWAYS; + op.compareMask = op.writeMask = 0xFFu; + op.reference = 0; + depthStencilState.front = depthStencilState.back = op; + } + // No colour attachment in this render pass, so no attachment states either. + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = dynamicStates; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = 2; + info.pStages = stages; + info.pVertexInputState = &vertex; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewport; + info.pRasterizationState = &raster; + info.pMultisampleState = &samples; + info.pDepthStencilState = &depthStencilState; + info.pColorBlendState = &blend; + info.pDynamicState = &dynamic; + info.layout = kit.pipelineLayout; + info.renderPass = pass; + return vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, pipeline); + } + + void RecordWireShaderResolveDraw(VkCommandBuffer commandBuffer, const WireShaderResolveKit& kit, + VkRenderPass pass, VkPipeline pipeline, VkFramebuffer framebuffer, + VkDescriptorSet descriptorSet, VkExtent2D attachmentExtent, VkRect2D rect) { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffer; + // The RECTANGLE is the render area, exactly as the render-pass arm's renderArea is - the + // scratch outside it is never read. + begin.renderArea = rect; + vkCmdBeginRenderPass(commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + // THE VIEWPORT IS THE WHOLE ATTACHMENT AND THE SCISSOR IS THE RECT, and the two are not + // interchangeable here: gl_FragCoord is what the fragment shader fetches by, so the + // clip-space-to-attachment mapping has to be the identity for every rectangle. A viewport + // shrunk onto the rect would slide the whole image instead of moving a window over it. + const VkViewport viewport{0.0f, 0.0f, static_cast(attachmentExtent.width), + static_cast(attachmentExtent.height), 0.0f, 1.0f}; + vkCmdSetViewport(commandBuffer, 0, 1, &viewport); + vkCmdSetScissor(commandBuffer, 0, 1, &rect); + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, kit.pipelineLayout, 0, 1, + &descriptorSet, 0, nullptr); + vkCmdDraw(commandBuffer, 3, 1, 0, 0); + vkCmdEndRenderPass(commandBuffer); + } + + namespace { + constexpr Uint32 kProbeExtent = 4; + constexpr Uint32 kProbeTexels = kProbeExtent * kProbeExtent; + // Every Vulkan device must support 4 samples on a depth/stencil attachment + // (framebufferDepthSampleCounts), and the CTS case the probe stands for fails at 2 and 4. + constexpr VkSampleCountFlagBits kProbeSamples = VK_SAMPLE_COUNT_4_BIT; + constexpr Float kClearDepth = 0.25f; + constexpr Float kSentinelDepth = 0.75f; + constexpr Uint32 kClearStencil = 0x5Au; + constexpr Uint32 kSentinelStencil = 0xA5u; + constexpr Uint64 kFenceTimeoutNs = 5ull * 1000ull * 1000ull * 1000ull; + + // The formats the wire maps GL's depth/stencil internal formats onto (VkTextureManager), + // packed first: the case that set all this off failed on every one of them. + struct ProbeFormat { + VkFormat format; + const char* name; + }; + constexpr ProbeFormat kProbeFormats[] = { + {VK_FORMAT_D24_UNORM_S8_UINT, "D24_UNORM_S8_UINT"}, + {VK_FORMAT_D32_SFLOAT_S8_UINT, "D32_SFLOAT_S8_UINT"}, + {VK_FORMAT_D16_UNORM, "D16_UNORM"}, + {VK_FORMAT_X8_D24_UNORM_PACK32, "X8_D24_UNORM_PACK32"}, + {VK_FORMAT_D32_SFLOAT, "D32_SFLOAT"}, + {VK_FORMAT_S8_UINT, "S8_UINT"}, + }; + + // The usages production gives the two images: a wire multisample depth/stencil attachment + // (VkTextureManager: SAMPLED | DEPTH_STENCIL_ATTACHMENT | TRANSFER_SRC | TRANSFER_DST) and + // ResolveWireDepthStencil's resolve scratch (DEPTH_STENCIL_ATTACHMENT | TRANSFER_SRC). + constexpr VkImageUsageFlags kSourceUsage = VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + constexpr VkImageUsageFlags kScratchUsage = + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + + Bool IsFloatDepth(VkFormat format) { + return format == VK_FORMAT_D32_SFLOAT || format == VK_FORMAT_D32_SFLOAT_S8_UINT; + } + + Uint32 DepthCopyBytes(VkFormat format) { return format == VK_FORMAT_D16_UNORM ? 2u : 4u; } + + Uint32 EncodeDepth(VkFormat format, Float depth) { + if (IsFloatDepth(format)) return std::bit_cast(depth); + const double scale = format == VK_FORMAT_D16_UNORM ? 65535.0 : 16777215.0; + return static_cast(std::lround(static_cast(depth) * scale)); + } + + Uint32 ReadDepthWord(VkFormat format, const Uint8* texel) { + if (format == VK_FORMAT_D16_UNORM) { + Uint16 word = 0; + std::memcpy(&word, texel, sizeof(word)); + return word; + } + Uint32 word = 0; + std::memcpy(&word, texel, sizeof(word)); + // A D24 aspect copies out as a 32-bit word whose top byte is undefined. + return IsFloatDepth(format) ? word : (word & 0x00FFFFFFu); + } + + Uint32 WordDistance(Uint32 a, Uint32 b) { return a > b ? a - b : b - a; } + + struct ProbeImage { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + }; + + struct ControlPass { + Bool ready = false; + ProbeImage target; + VkRenderPass pass = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; + VkImageView sampledView = VK_NULL_HANDLE; + VkImageView targetView = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkDeviceSize offset = 0; + }; + + struct FormatWork { + SizeT reading = 0; + VkFormat format = VK_FORMAT_UNDEFINED; + VkImageAspectFlags aspects = 0; + ProbeImage subjectSource, controlSource, subjectTarget; + VkRenderPass resolvePass = VK_NULL_HANDLE; + VkImageView resolveViews[2]{}; + VkFramebuffer resolveFramebuffer = VK_NULL_HANDLE; + VkDeviceSize subjectDepthOffset = 0, subjectStencilOffset = 0; + ControlPass depthControl, stencilControl; + }; + + // Everything the probe creates, destroyed together (or leaked together on a timeout). + struct ProbeObjects { + VkDevice device = VK_NULL_HANDLE; + Vector images; + Vector memories; + Vector views; + Vector passes; + Vector framebuffers; + Vector pipelines; + VkBuffer buffer = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkCommandPool commandPool = VK_NULL_HANDLE; + VkFence fence = VK_NULL_HANDLE; + WireShaderResolveKit kit; + + void Destroy() { + for (VkPipeline pipeline : pipelines) vkDestroyPipeline(device, pipeline, nullptr); + for (VkFramebuffer framebuffer : framebuffers) vkDestroyFramebuffer(device, framebuffer, nullptr); + for (VkRenderPass pass : passes) vkDestroyRenderPass(device, pass, nullptr); + for (VkImageView view : views) vkDestroyImageView(device, view, nullptr); + for (VkImage image : images) vkDestroyImage(device, image, nullptr); + vkDestroyBuffer(device, buffer, nullptr); + for (VkDeviceMemory memory : memories) vkFreeMemory(device, memory, nullptr); + vkDestroyDescriptorPool(device, descriptorPool, nullptr); + vkDestroyCommandPool(device, commandPool, nullptr); + vkDestroyFence(device, fence, nullptr); + DestroyWireShaderResolveKit(device, kit); + } + }; + + Optional PickMemoryType(const VkPhysicalDeviceMemoryProperties& properties, Uint32 allowed, + VkMemoryPropertyFlags wanted, VkMemoryPropertyFlags required) { + Optional fallback; + for (Uint32 i = 0; i < properties.memoryTypeCount; ++i) { + if (!(allowed & (1u << i))) continue; + const VkMemoryPropertyFlags flags = properties.memoryTypes[i].propertyFlags; + if (flags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) continue; + if ((flags & required) != required) continue; + if ((flags & wanted) == wanted) return i; + if (!fallback) fallback = i; + } + return fallback; + } + + void ImageBarrier(VkCommandBuffer commandBuffer, VkImage image, VkImageAspectFlags aspects, + VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags srcStage, + VkAccessFlags srcAccess, VkPipelineStageFlags dstStage, VkAccessFlags dstAccess) { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = srcAccess; + barrier.dstAccessMask = dstAccess; + barrier.oldLayout = oldLayout; + barrier.newLayout = newLayout; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {aspects, 0, 1, 0, VK_REMAINING_ARRAY_LAYERS}; + vkCmdPipelineBarrier(commandBuffer, srcStage, dstStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); + } + } // namespace + + WireDepthResolveProbeMeasurement RunWireDepthResolveProbe(const WireDepthResolveProbeContext& context) { + WireDepthResolveProbeMeasurement measurement; + const VkDevice device = context.device; + if (device == VK_NULL_HANDLE || context.queue == VK_NULL_HANDLE || context.createRenderPass2 == nullptr) { + measurement.failureReason = "no device, queue or vkCreateRenderPass2"; + return measurement; + } + ProbeObjects objects; + objects.device = device; + Bool leak = false; + struct Teardown { + ProbeObjects& objects; + Bool& leak; + ~Teardown() { + if (!leak) objects.Destroy(); + } + } teardown{objects, leak}; + const auto fail = [&](String reason) { + measurement.ran = false; + measurement.failureReason = Move(reason); + return measurement; + }; + + VkPhysicalDeviceMemoryProperties memoryProperties{}; + vkGetPhysicalDeviceMemoryProperties(context.physicalDevice, &memoryProperties); + const auto makeImage = [&](VkFormat format, VkSampleCountFlagBits samples, VkImageUsageFlags usage, + ProbeImage& out) -> Bool { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {kProbeExtent, kProbeExtent, 1}; + info.mipLevels = info.arrayLayers = 1; + info.samples = samples; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + if (vkCreateImage(device, &info, nullptr, &out.image) != VK_SUCCESS) return false; + objects.images.push_back(out.image); + VkMemoryRequirements requirements{}; + vkGetImageMemoryRequirements(device, out.image, &requirements); + const Optional type = PickMemoryType(memoryProperties, requirements.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0); + if (!type) return false; + VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocate.allocationSize = requirements.size; + allocate.memoryTypeIndex = *type; + if (vkAllocateMemory(device, &allocate, nullptr, &out.memory) != VK_SUCCESS) return false; + objects.memories.push_back(out.memory); + return vkBindImageMemory(device, out.image, out.memory, 0) == VK_SUCCESS; + }; + const auto makeView = [&](const VkImageViewCreateInfo& info, VkImageView& out) -> Bool { + if (vkCreateImageView(device, &info, nullptr, &out) != VK_SUCCESS) return false; + objects.views.push_back(out); + return true; + }; + const auto makeFramebuffer = [&](VkRenderPass pass, const VkImageView* views, Uint32 count, + VkFramebuffer& out) -> Bool { + VkFramebufferCreateInfo info{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + info.renderPass = pass; + info.attachmentCount = count; + info.pAttachments = views; + info.width = info.height = kProbeExtent; + info.layers = 1; + if (vkCreateFramebuffer(device, &info, nullptr, &out) != VK_SUCCESS) return false; + objects.framebuffers.push_back(out); + return true; + }; + // The wire's clear (WireFramebuffer.inc, ClearWireFramebuffer): a LOAD/STORE pass over every + // aspect of the format with vkCmdClearAttachments inside it. Recorded later; built now. + struct ClearPass { + VkRenderPass pass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkImageAspectFlags aspects = 0; + Float depth = 0.0f; + Uint32 stencil = 0; + }; + Vector clears; + const auto makeClear = [&](const ProbeImage& image, VkFormat format, VkSampleCountFlagBits samples, + Float depth, Uint32 stencil) -> Bool { + const VkImageAspectFlags aspects = WireDepthStencilFormatAspects(format); + VkImageView view = VK_NULL_HANDLE; + if (!makeView(WireDepthStencilViewInfo(image.image, format, aspects, 0, 0), view)) return false; + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = samples; + attachment.loadOp = attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &reference; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = 1; + info.pAttachments = &attachment; + info.subpassCount = 1; + info.pSubpasses = &subpass; + ClearPass clear; + if (vkCreateRenderPass(device, &info, nullptr, &clear.pass) != VK_SUCCESS) return false; + objects.passes.push_back(clear.pass); + if (!makeFramebuffer(clear.pass, &view, 1, clear.framebuffer)) return false; + clear.aspects = aspects; + clear.depth = depth; + clear.stencil = stencil; + clears.push_back(clear); + return true; + }; + + // THE CONTROL'S FORMAT-INDEPENDENT HALF. A kit that cannot be made leaves every control + // un-run, which the verdict reads as INCONCLUSIVE - never as the bug. + String kitFailure; + if (const VkResult result = CreateWireShaderResolveKit(device, context.shaderStencilExport, objects.kit); + result != VK_SUCCESS) + kitFailure = format("shader resolve kit creation failed (VkResult {})", static_cast(result)); + + // Readback layout: every region on a 16-byte boundary (a depth/stencil buffer copy wants a + // multiple of 4). + VkDeviceSize bufferBytes = 0; + const auto reserve = [&](VkDeviceSize bytes) { + const VkDeviceSize offset = bufferBytes; + bufferBytes += (bytes + 15u) & ~VkDeviceSize{15u}; + return offset; + }; + + Vector work; + for (const ProbeFormat& probeFormat : kProbeFormats) { + WireDepthResolveFormatReading reading; + reading.name = probeFormat.name; + reading.format = static_cast(probeFormat.format); + reading.samples = static_cast(kProbeSamples); + const SizeT readingIndex = measurement.formats.size(); + measurement.formats.push_back(reading); + auto& slot = measurement.formats.back(); + // A skipped format records nothing: the clears it had already built are dropped with it + // (their images stay in `objects` and are only destroyed). + const SizeT clearsBefore = clears.size(); + const auto skip = [&](String reason) { + slot.skipReason = Move(reason); + clears.resize(clearsBefore); + }; + + const VkFormat format = probeFormat.format; + const VkImageAspectFlags aspects = WireDepthStencilFormatAspects(format); + VkFormatProperties formatProperties{}; + vkGetPhysicalDeviceFormatProperties(context.physicalDevice, format, &formatProperties); + constexpr VkFormatFeatureFlags kRequired = + VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + if ((formatProperties.optimalTilingFeatures & kRequired) != kRequired) { + skip("not a sampled depth/stencil attachment format here"); + continue; + } + VkImageFormatProperties imageProperties{}; + if (vkGetPhysicalDeviceImageFormatProperties(context.physicalDevice, format, VK_IMAGE_TYPE_2D, + VK_IMAGE_TILING_OPTIMAL, kSourceUsage, 0, + &imageProperties) != VK_SUCCESS || + !(imageProperties.sampleCounts & kProbeSamples)) { + skip("no 4x multisample image with the wire's attachment usage"); + continue; + } + if (vkGetPhysicalDeviceImageFormatProperties(context.physicalDevice, format, VK_IMAGE_TYPE_2D, + VK_IMAGE_TILING_OPTIMAL, kScratchUsage, 0, + &imageProperties) != VK_SUCCESS) { + skip("no single-sample image with the resolve scratch's usage"); + continue; + } + // ResolveWireDepthStencil's own test for the render-pass arm, per format. + if (((aspects & VK_IMAGE_ASPECT_DEPTH_BIT) && + !(context.depthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)) || + ((aspects & VK_IMAGE_ASPECT_STENCIL_BIT) && + !(context.stencilResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT))) { + skip("the render-pass arm is not taken for this format (no SAMPLE_ZERO resolve mode)"); + continue; + } + + FormatWork item; + item.reading = readingIndex; + item.format = format; + item.aspects = aspects; + if (!makeImage(format, kProbeSamples, kSourceUsage, item.subjectSource) || + !makeImage(format, kProbeSamples, kSourceUsage, item.controlSource) || + !makeImage(format, VK_SAMPLE_COUNT_1_BIT, kScratchUsage, item.subjectTarget)) { + skip("image creation failed"); + continue; + } + if (CreateWireDepthStencilResolvePass(context.createRenderPass2, device, format, kProbeSamples, + &item.resolvePass) != VK_SUCCESS) { + skip("the render-pass arm's vkCreateRenderPass2 failed"); + continue; + } + objects.passes.push_back(item.resolvePass); + if (!makeView(WireDepthStencilViewInfo(item.subjectSource.image, format, aspects, 0, 0), + item.resolveViews[0]) || + !makeView(WireDepthStencilViewInfo(item.subjectTarget.image, format, aspects, 0, 0), + item.resolveViews[1]) || + !makeFramebuffer(item.resolvePass, item.resolveViews, 2, item.resolveFramebuffer)) { + skip("the render-pass arm's views or framebuffer could not be created"); + continue; + } + if (!makeClear(item.subjectSource, format, kProbeSamples, kClearDepth, kClearStencil) || + !makeClear(item.controlSource, format, kProbeSamples, kClearDepth, kClearStencil) || + !makeClear(item.subjectTarget, format, VK_SAMPLE_COUNT_1_BIT, kSentinelDepth, kSentinelStencil)) { + skip("the clear passes could not be created"); + continue; + } + // The control, one target per aspect: the shader pass's attachment is DONT_CARE on both + // aspects, so a second aspect's pass into the same target would discard the first. + const auto makeControl = [&](VkImageAspectFlags aspect, ControlPass& control) { + if (!kitFailure.empty()) return; + if (aspect == VK_IMAGE_ASPECT_STENCIL_BIT && objects.kit.stencilFragment == VK_NULL_HANDLE) return; + if (!makeImage(format, VK_SAMPLE_COUNT_1_BIT, kScratchUsage, control.target)) return; + if (CreateWireShaderResolveRenderPass(device, format, &control.pass) != VK_SUCCESS) return; + objects.passes.push_back(control.pass); + if (CreateWireShaderResolvePipeline(device, objects.kit, control.pass, aspect, &control.pipeline) != + VK_SUCCESS) + return; + objects.pipelines.push_back(control.pipeline); + if (!makeView(WireDepthStencilViewInfo(item.controlSource.image, format, aspect, 0, 0), + control.sampledView) || + !makeView(WireDepthStencilViewInfo(control.target.image, format, aspects, 0, 0), + control.targetView) || + !makeFramebuffer(control.pass, &control.targetView, 1, control.framebuffer)) + return; + if (!makeClear(control.target, format, VK_SAMPLE_COUNT_1_BIT, kSentinelDepth, kSentinelStencil)) + return; + control.ready = true; + }; + if (aspects & VK_IMAGE_ASPECT_DEPTH_BIT) makeControl(VK_IMAGE_ASPECT_DEPTH_BIT, item.depthControl); + if (aspects & VK_IMAGE_ASPECT_STENCIL_BIT) makeControl(VK_IMAGE_ASPECT_STENCIL_BIT, item.stencilControl); + + if (aspects & VK_IMAGE_ASPECT_DEPTH_BIT) { + item.subjectDepthOffset = reserve(kProbeTexels * DepthCopyBytes(format)); + if (item.depthControl.ready) item.depthControl.offset = reserve(kProbeTexels * DepthCopyBytes(format)); + } + if (aspects & VK_IMAGE_ASPECT_STENCIL_BIT) { + item.subjectStencilOffset = reserve(kProbeTexels); + if (item.stencilControl.ready) item.stencilControl.offset = reserve(kProbeTexels); + } + slot.ran = true; + work.push_back(item); + } + if (work.empty()) return fail("no format could be probed"); + + // Descriptor sets for the controls, one per draw (the production shader arm's own rule). + Uint32 controlDraws = 0; + for (const FormatWork& item : work) + controlDraws += (item.depthControl.ready ? 1u : 0u) + (item.stencilControl.ready ? 1u : 0u); + if (controlDraws > 0) { + const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, controlDraws}; + VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + pool.maxSets = controlDraws; + pool.poolSizeCount = 1; + pool.pPoolSizes = &poolSize; + if (vkCreateDescriptorPool(device, &pool, nullptr, &objects.descriptorPool) != VK_SUCCESS) + return fail("vkCreateDescriptorPool failed"); + for (FormatWork& item : work) { + for (ControlPass* control : {&item.depthControl, &item.stencilControl}) { + if (!control->ready) continue; + VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + alloc.descriptorPool = objects.descriptorPool; + alloc.descriptorSetCount = 1; + alloc.pSetLayouts = &objects.kit.descriptorLayout; + if (vkAllocateDescriptorSets(device, &alloc, &control->set) != VK_SUCCESS) + return fail("vkAllocateDescriptorSets failed"); + const VkDescriptorImageInfo sampled{objects.kit.nearest, control->sampledView, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = control->set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &sampled; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + } + } + } + + // The host-visible readback buffer. + VkDeviceMemory bufferMemory = VK_NULL_HANDLE; + Bool bufferCoherent = false; + { + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + info.size = bufferBytes; + info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(device, &info, nullptr, &objects.buffer) != VK_SUCCESS) + return fail("vkCreateBuffer failed"); + VkMemoryRequirements requirements{}; + vkGetBufferMemoryRequirements(device, objects.buffer, &requirements); + const Optional type = + PickMemoryType(memoryProperties, requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); + if (!type) return fail("no host-visible memory type for the readback buffer"); + bufferCoherent = (memoryProperties.memoryTypes[*type].propertyFlags & + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0; + VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocate.allocationSize = requirements.size; + allocate.memoryTypeIndex = *type; + if (vkAllocateMemory(device, &allocate, nullptr, &bufferMemory) != VK_SUCCESS) + return fail("vkAllocateMemory (readback) failed"); + objects.memories.push_back(bufferMemory); + if (vkBindBufferMemory(device, objects.buffer, bufferMemory, 0) != VK_SUCCESS) + return fail("vkBindBufferMemory failed"); + } + + VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + poolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; + poolInfo.queueFamilyIndex = context.queueFamilyIndex; + if (vkCreateCommandPool(device, &poolInfo, nullptr, &objects.commandPool) != VK_SUCCESS) + return fail("vkCreateCommandPool failed"); + VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + commandInfo.commandPool = objects.commandPool; + commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + commandInfo.commandBufferCount = 1; + VkCommandBuffer cmd = VK_NULL_HANDLE; + if (vkAllocateCommandBuffers(device, &commandInfo, &cmd) != VK_SUCCESS) + return fail("vkAllocateCommandBuffers failed"); + VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + if (vkBeginCommandBuffer(cmd, &beginInfo) != VK_SUCCESS) return fail("vkBeginCommandBuffer failed"); + + const VkRect2D fullRect{{0, 0}, {kProbeExtent, kProbeExtent}}; + // (1) Every image UNDEFINED -> DEPTH_STENCIL_ATTACHMENT_OPTIMAL, the way TransitionWireImage + // moves a fresh image (source state from UNDEFINED, destination ALL_COMMANDS). + for (const FormatWork& item : work) { + const VkImage images[] = {item.subjectSource.image, item.controlSource.image, item.subjectTarget.image, + item.depthControl.ready ? item.depthControl.target.image : VK_NULL_HANDLE, + item.stencilControl.ready ? item.stencilControl.target.image : VK_NULL_HANDLE}; + for (const VkImage image : images) { + if (image == VK_NULL_HANDLE) continue; + ImageBarrier(cmd, image, item.aspects, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + } + } + // (2) The inputs and the sentinels, cleared the wire's way. + for (const ClearPass& clear : clears) { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = clear.pass; + begin.framebuffer = clear.framebuffer; + begin.renderArea = fullRect; + vkCmdBeginRenderPass(cmd, &begin, VK_SUBPASS_CONTENTS_INLINE); + VkClearAttachment attachment{}; + attachment.aspectMask = clear.aspects; + attachment.clearValue.depthStencil = {clear.depth, clear.stencil}; + const VkClearRect rect{fullRect, 0, 1}; + vkCmdClearAttachments(cmd, 1, &attachment, 1, &rect); + vkCmdEndRenderPass(cmd); + } + // (3) What TransitionWireImage records when the layout already matches - the case of a + // source the application cleared and then resolves. + { + VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, + &memory, 0, nullptr, 0, nullptr); + } + // (4) THE SUBJECT: the render-pass arm, begun and ended with no draw. + for (const FormatWork& item : work) { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = item.resolvePass; + begin.framebuffer = item.resolveFramebuffer; + begin.renderArea = fullRect; + vkCmdBeginRenderPass(cmd, &begin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdEndRenderPass(cmd); + } + // (5) THE CONTROL: the shader arm on the twin source, which TransitionWireImage moves to + // SHADER_READ_ONLY_OPTIMAL first (both aspects of a packed format). + for (const FormatWork& item : work) { + if (!item.depthControl.ready && !item.stencilControl.ready) continue; + ImageBarrier(cmd, item.controlSource.image, item.aspects, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + for (const ControlPass* control : {&item.depthControl, &item.stencilControl}) { + if (!control->ready) continue; + RecordWireShaderResolveDraw(cmd, objects.kit, control->pass, control->pipeline, control->framebuffer, + control->set, {kProbeExtent, kProbeExtent}, fullRect); + } + } + // (6) Every target to TRANSFER_SRC with the barrier ResolveWireDepthStencil puts after its + // resolve (a depth/stencil resolve writes in COLOR_ATTACHMENT_OUTPUT), then one copy per aspect. + const auto copyOut = [&](VkImage image, VkImageAspectFlags aspect, VkDeviceSize offset) { + VkBufferImageCopy copy{}; + copy.bufferOffset = offset; + copy.imageSubresource = {aspect, 0, 0, 1}; + copy.imageExtent = {kProbeExtent, kProbeExtent, 1}; + vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, objects.buffer, 1, ©); + }; + const auto toTransfer = [&](VkImage image, VkImageAspectFlags aspects) { + ImageBarrier(cmd, image, aspects, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); + }; + for (const FormatWork& item : work) { + toTransfer(item.subjectTarget.image, item.aspects); + if (item.aspects & VK_IMAGE_ASPECT_DEPTH_BIT) + copyOut(item.subjectTarget.image, VK_IMAGE_ASPECT_DEPTH_BIT, item.subjectDepthOffset); + if (item.aspects & VK_IMAGE_ASPECT_STENCIL_BIT) + copyOut(item.subjectTarget.image, VK_IMAGE_ASPECT_STENCIL_BIT, item.subjectStencilOffset); + if (item.depthControl.ready) { + toTransfer(item.depthControl.target.image, item.aspects); + copyOut(item.depthControl.target.image, VK_IMAGE_ASPECT_DEPTH_BIT, item.depthControl.offset); + } + if (item.stencilControl.ready) { + toTransfer(item.stencilControl.target.image, item.aspects); + copyOut(item.stencilControl.target.image, VK_IMAGE_ASPECT_STENCIL_BIT, item.stencilControl.offset); + } + } + { + VkBufferMemoryBarrier barrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.buffer = objects.buffer; + barrier.size = VK_WHOLE_SIZE; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, + &barrier, 0, nullptr); + } + if (vkEndCommandBuffer(cmd) != VK_SUCCESS) return fail("vkEndCommandBuffer failed"); + + VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + if (vkCreateFence(device, &fenceInfo, nullptr, &objects.fence) != VK_SUCCESS) + return fail("vkCreateFence failed"); + VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &cmd; + if (const VkResult result = vkQueueSubmit(context.queue, 1, &submit, objects.fence); result != VK_SUCCESS) + return fail(format("vkQueueSubmit failed (VkResult {})", static_cast(result))); + const VkResult waited = vkWaitForFences(device, 1, &objects.fence, VK_TRUE, kFenceTimeoutNs); + if (waited == VK_TIMEOUT) { + // The submission may still be executing: destroying anything it names, or idle-waiting + // the device, is the hang the bound exists to prevent. Everything leaks. + leak = true; + measurement.fenceWaitTimedOut = true; + return fail("the probe's fence did not signal within 5 s"); + } + if (waited != VK_SUCCESS) return fail(format("vkWaitForFences failed (VkResult {})", static_cast(waited))); + + void* mapped = nullptr; + if (vkMapMemory(device, bufferMemory, 0, VK_WHOLE_SIZE, 0, &mapped) != VK_SUCCESS || mapped == nullptr) + return fail("vkMapMemory failed"); + if (!bufferCoherent) { + VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; + range.memory = bufferMemory; + range.size = VK_WHOLE_SIZE; + vkInvalidateMappedMemoryRanges(device, 1, &range); + } + const Uint8* bytes = static_cast(mapped); + const auto tally = [&](WireDepthResolveAspectReading& aspect, VkDeviceSize offset, Uint32 texelBytes, + Bool depth, VkFormat format, Bool subject) { + Uint32 matches = 0, sentinels = 0, first = 0; + for (Uint32 i = 0; i < kProbeTexels; ++i) { + const Uint8* texel = bytes + offset + static_cast(i) * texelBytes; + const Uint32 word = depth ? ReadDepthWord(format, texel) : static_cast(*texel); + if (i == 0) first = word; + if (WordDistance(word, aspect.expected) <= aspect.tolerance) ++matches; + if (WordDistance(word, aspect.sentinel) <= aspect.tolerance) ++sentinels; + } + if (subject) { + aspect.renderPassMatches = matches; + aspect.renderPassSentinels = sentinels; + aspect.renderPassFirst = first; + } else { + aspect.shaderRan = true; + aspect.shaderMatches = matches; + aspect.shaderFirst = first; + } + }; + for (const FormatWork& item : work) { + WireDepthResolveFormatReading& reading = measurement.formats[item.reading]; + if (item.aspects & VK_IMAGE_ASPECT_DEPTH_BIT) { + auto& aspect = reading.depth; + aspect.measured = true; + aspect.texels = kProbeTexels; + aspect.expected = EncodeDepth(item.format, kClearDepth); + aspect.sentinel = EncodeDepth(item.format, kSentinelDepth); + aspect.tolerance = IsFloatDepth(item.format) ? 0u : 1u; + tally(aspect, item.subjectDepthOffset, DepthCopyBytes(item.format), true, item.format, true); + if (item.depthControl.ready) + tally(aspect, item.depthControl.offset, DepthCopyBytes(item.format), true, item.format, false); + } + if (item.aspects & VK_IMAGE_ASPECT_STENCIL_BIT) { + auto& aspect = reading.stencil; + aspect.measured = true; + aspect.texels = kProbeTexels; + aspect.expected = kClearStencil; + aspect.sentinel = kSentinelStencil; + aspect.tolerance = 0; + tally(aspect, item.subjectStencilOffset, 1, false, item.format, true); + if (item.stencilControl.ready) + tally(aspect, item.stencilControl.offset, 1, false, item.format, false); + } + } + vkUnmapMemory(device, bufferMemory); + measurement.ran = true; + if (!kitFailure.empty()) measurement.failureReason = kitFailure; + return measurement; + } + + WireDepthResolveProbeMeasurement RunWireDepthResolveProbeOnThrowawayDevice( + PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice, + Uint32 graphicsQueueFamilyIndex, const Vector& deviceExtensions, + Bool& renderPassArmAvailable, Bool& shaderStencilExport) { + WireDepthResolveProbeMeasurement measurement; + const auto has = [&](const char* name) { + for (const VkExtensionProperties& extension : deviceExtensions) + if (std::strcmp(extension.extensionName, name) == 0) return true; + return false; + }; + renderPassArmAvailable = has(VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME) && + has(VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME); + shaderStencilExport = has(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); + if (!renderPassArmAvailable) { + measurement.failureReason = "no VK_KHR_create_renderpass2 + VK_KHR_depth_stencil_resolve"; + return measurement; + } + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(physicalDevice, &properties); + auto getProperties2 = reinterpret_cast( + getInstanceProcAddr(instance, "vkGetPhysicalDeviceProperties2")); + if (getProperties2 == nullptr) + getProperties2 = reinterpret_cast( + getInstanceProcAddr(instance, "vkGetPhysicalDeviceProperties2KHR")); + const auto createDevice = + reinterpret_cast(getInstanceProcAddr(instance, "vkCreateDevice")); + if (properties.apiVersion < VK_API_VERSION_1_1 || getProperties2 == nullptr || createDevice == nullptr) { + renderPassArmAvailable = false; + measurement.failureReason = "a pre-1.1 device, or no vkGetPhysicalDeviceProperties2 / vkCreateDevice"; + return measurement; + } + VkPhysicalDeviceDepthStencilResolveProperties resolveProperties{ + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_STENCIL_RESOLVE_PROPERTIES}; + VkPhysicalDeviceProperties2 properties2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2}; + properties2.pNext = &resolveProperties; + getProperties2(physicalDevice, &properties2); + + // The renderer's own extension set for the two arms (VulkanRenderer.cpp, + // CreateLogicalDeviceAndQueues), nothing more. + const Float priority = 1.0f; + VkDeviceQueueCreateInfo queueInfo{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex; + queueInfo.queueCount = 1; + queueInfo.pQueuePriorities = &priority; + Vector extensions{VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME, + VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME}; + if (shaderStencilExport) extensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); + VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + deviceInfo.queueCreateInfoCount = 1; + deviceInfo.pQueueCreateInfos = &queueInfo; + deviceInfo.enabledExtensionCount = static_cast(extensions.size()); + deviceInfo.ppEnabledExtensionNames = extensions.data(); + VkDevice device = VK_NULL_HANDLE; + if (const VkResult result = createDevice(physicalDevice, &deviceInfo, nullptr, &device); + result != VK_SUCCESS || device == VK_NULL_HANDLE) { + measurement.failureReason = format("vkCreateDevice failed (VkResult {})", static_cast(result)); + return measurement; + } + WireDepthResolveProbeContext context; + context.physicalDevice = physicalDevice; + context.device = device; + vkGetDeviceQueue(device, graphicsQueueFamilyIndex, 0, &context.queue); + context.queueFamilyIndex = graphicsQueueFamilyIndex; + context.createRenderPass2 = + reinterpret_cast(vkGetDeviceProcAddr(device, "vkCreateRenderPass2KHR")); + context.depthResolveModes = resolveProperties.supportedDepthResolveModes; + context.stencilResolveModes = resolveProperties.supportedStencilResolveModes; + context.shaderStencilExport = shaderStencilExport; + measurement = RunWireDepthResolveProbe(context); + // A timed-out probe leaked its children on purpose; destroying their device would block + // exactly where the bound says not to. + if (!measurement.fenceWaitTimedOut) vkDestroyDevice(device, nullptr); + return measurement; + } +} // namespace MobileGL::MG_Backend::DirectVulkan +#endif diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.h new file mode 100644 index 00000000..13c7cb91 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.h @@ -0,0 +1,117 @@ +// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.h +// Copyright (c) 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 "WireDepthResolveArm.h" + +#include +#include + +// P7 gate 5 (g5-msprobe): the Vulkan half of the multisample depth/stencil resolve probe, and the +// two resolve arms it measures. +// +// THE ARMS LIVE HERE, NOT ONLY IN THE RENDERER. A probe that rebuilt "something like" the wire's +// render pass would measure its own copy; the render-pass description, the shader pass's modules, +// pipeline and draw, and the view shapes both arms attach are defined once below and called by +// ResolveWireDepthStencil (WireFramebuffer.inc), ResolveWireDepthStencilWithShader +// (WireMultisampleResolve.inc) and the probe alike. +// +// Two callers: the Magma renderer at device bring-up (server side, once per process, only where +// the render-pass arm exists at all - VulkanRenderer::ArmWireDepthResolveOrder) and the POST's +// "Known Driver Bugs" section (MG_Util/SelfTest/DriverBugProbes.cpp) on a throwaway device. +// +// Disaggregated builds only: the monolith never records either arm, and the pull build's image +// must not change (P7 gate G1). +#if MOBILEGL_BUILD_DISAGGREGATED +namespace MobileGL::MG_Backend::DirectVulkan { + // Every aspect a wire depth/stencil format has. + VkImageAspectFlags WireDepthStencilFormatAspects(VkFormat format); + + // A 2D view of one level/layer of a depth/stencil image naming `aspects`. The render-pass arm + // attaches both images with every aspect of the format; the shader arm SAMPLES its source + // through a view of exactly one aspect (a depth/stencil image can only be sampled that way) and + // attaches its target with every aspect. + VkImageViewCreateInfo WireDepthStencilViewInfo(VkImage image, VkFormat format, VkImageAspectFlags aspects, + Uint32 level, Uint32 layer); + + // THE RENDER-PASS ARM: attachment 0 the multisample source (LOAD/STORE), attachment 1 the + // single-sample resolve target (DONT_CARE/STORE), both in DEPTH_STENCIL_ATTACHMENT_OPTIMAL, one + // subpass whose VkSubpassDescriptionDepthStencilResolve resolves every aspect of the format + // with VK_RESOLVE_MODE_SAMPLE_ZERO_BIT. The caller begins and ends it with NO draw. + VkResult CreateWireDepthStencilResolvePass(PFN_vkCreateRenderPass2 createRenderPass2, VkDevice device, + VkFormat format, VkSampleCountFlagBits samples, VkRenderPass* pass); + + // THE SHADER ARM's device objects that do not depend on the format. + struct WireShaderResolveKit { + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule depthFragment = VK_NULL_HANDLE; + // Only where VK_EXT_shader_stencil_export is enabled: its SPIR-V declares StencilExportEXT. + VkShaderModule stencilFragment = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkSampler nearest = VK_NULL_HANDLE; + }; + // Creates every member; the first failure is returned and the members made so far stay set, + // for DestroyWireShaderResolveKit. + VkResult CreateWireShaderResolveKit(VkDevice device, Bool shaderStencilExport, WireShaderResolveKit& kit); + void DestroyWireShaderResolveKit(VkDevice device, WireShaderResolveKit& kit); + // One (format, aspect)'s single-attachment render pass: DONT_CARE/STORE on both aspects, + // DEPTH_STENCIL_ATTACHMENT_OPTIMAL throughout. + VkResult CreateWireShaderResolveRenderPass(VkDevice device, VkFormat format, VkRenderPass* pass); + // Its pipeline: full-screen triangle, gl_FragDepth (ALWAYS + write) or gl_FragStencilRefARB + // (REPLACE everywhere), viewport and scissor dynamic. VK_ERROR_FEATURE_NOT_PRESENT when the kit + // has no fragment module for `aspect`. + VkResult CreateWireShaderResolvePipeline(VkDevice device, const WireShaderResolveKit& kit, VkRenderPass pass, + VkImageAspectFlags aspect, VkPipeline* pipeline); + // Records the draw: the render area and the scissor are `rect`, the viewport is the WHOLE + // attachment (gl_FragCoord is what the fragment stage fetches by), one triangle. + void RecordWireShaderResolveDraw(VkCommandBuffer commandBuffer, const WireShaderResolveKit& kit, + VkRenderPass pass, VkPipeline pipeline, VkFramebuffer framebuffer, + VkDescriptorSet descriptorSet, VkExtent2D attachmentExtent, VkRect2D rect); + + // What the probe runs on. The device must have been created with VK_KHR_create_renderpass2 and + // VK_KHR_depth_stencil_resolve (or Vulkan 1.2 core), and with VK_EXT_shader_stencil_export where + // `shaderStencilExport` says so. The queue must be otherwise idle for the probe's duration. + struct WireDepthResolveProbeContext { + VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + VkQueue queue = VK_NULL_HANDLE; + Uint32 queueFamilyIndex = 0; + PFN_vkCreateRenderPass2 createRenderPass2 = nullptr; + VkResolveModeFlags depthResolveModes = 0; + VkResolveModeFlags stencilResolveModes = 0; + Bool shaderStencilExport = false; + }; + + // Per format the device can host as a 4x multisample depth/stencil attachment AND for which + // ResolveWireDepthStencil would take the render-pass arm (D16, X8_D24, D32F, D24S8, D32FS8, S8): + // - two identical 4x4 4x multisample sources (production usage), each cleared to depth 0.25 / + // stencil 0x5A the way the wire clears (a LOAD pass with vkCmdClearAttachments); + // - single-sample resolve targets (the production scratch usage) pre-filled with the sentinel + // depth 0.75 / stencil 0xA5; + // - SUBJECT: the render-pass arm's exact pass, begun and ended with no draw; + // - CONTROL: the shader arm's exact pass on the other source, one target per aspect (stencil + // only with VK_EXT_shader_stencil_export); + // - every target copied aspect by aspect into a host-visible buffer and compared texel by texel. + // One submission, a bounded fence wait (on timeout every object is leaked and the measurement + // says so - the caller must then not destroy or idle-wait the device), teardown on every other + // path. Never touches renderer state. + WireDepthResolveProbeMeasurement RunWireDepthResolveProbe(const WireDepthResolveProbeContext& context); + + // The POST's entry: creates a throwaway device on `physicalDevice` with exactly the extensions + // the renderer enables for the two arms, runs the probe on its graphics queue and destroys the + // device (leaks it on a fence timeout). `renderPassArmAvailable` is false - and nothing is run - + // when the device lacks VK_KHR_create_renderpass2 / VK_KHR_depth_stencil_resolve; the shader + // pass is then the only arm and the probe has nothing to choose between. + // `shaderStencilExport` reports whether VK_EXT_shader_stencil_export was available. + WireDepthResolveProbeMeasurement RunWireDepthResolveProbeOnThrowawayDevice( + PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice, + Uint32 graphicsQueueFamilyIndex, const Vector& deviceExtensions, + Bool& renderPassArmAvailable, Bool& shaderStencilExport); +} // namespace MobileGL::MG_Backend::DirectVulkan +#endif diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index dfd37cf8..9cde2d8a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -669,11 +669,7 @@ static_cast(dxMin + width) > destination.extent.width || static_cast(dyMin + height) > destination.extent.height) MagmaWireFatal("multisample-depth-resolve-region"); - VkImageAspectFlags formatAspects = VK_IMAGE_ASPECT_DEPTH_BIT; - if (source.format == VK_FORMAT_S8_UINT) formatAspects = VK_IMAGE_ASPECT_STENCIL_BIT; - else if (source.format == VK_FORMAT_D16_UNORM_S8_UINT || source.format == VK_FORMAT_D24_UNORM_S8_UINT || - source.format == VK_FORMAT_D32_SFLOAT_S8_UINT) - formatAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + const VkImageAspectFlags formatAspects = WireDepthStencilFormatAspects(source.format); // P7 wave 2-B2, CONTRACT-P7 ยง3.2 (`multisample-blit-aspect`): TWO ARMS, and which one // runs is a fact about the DEVICE, not about the shape. VK_KHR_depth_stencil_resolve is // the direct route and is taken wherever it exists; where it does not - which is where @@ -726,30 +722,32 @@ VkImageView views[2]{}; VkRenderPass pass = VK_NULL_HANDLE; VkFramebuffer framebuffer = VK_NULL_HANDLE; - // P7 gate 5 (g5-msrbo): WHERE THE EXTENSION ARM IS KNOWN NOT TO RESOLVE, THE SHADER ARM - // GOES FIRST. The extension arm is a render pass with no draw in it, and on the Redmi - // (Adreno 830, driver 512.800.71) that pass left the resolve target unwritten: + // P7 gate 5 (g5-msrbo, g5-msprobe): WHERE THE EXTENSION ARM IS MEASURED NOT TO RESOLVE, + // THE SHADER ARM GOES FIRST. The extension arm is a render pass with no draw in it, and on + // the Redmi (Adreno 830, driver 512.800.71) that pass left the resolve target unwritten: // KHR-GL46.direct_state_access.renderbuffers_storage_multisample read every resolved depth // and stencil value back as 0 on the inproc arm - exactly its samples 2/4/4 iterations, // every format - and the same case PASSED on that device with // MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=1. The shader arm draws, so nothing about it // can be elided. It still declines what it cannot express (a stencil aspect without // VK_EXT_shader_stencil_export), and then the extension arm is the fallback rather than - // a decline. m_wirePreferShaderDepthResolve is WirePrefersShaderDepthResolve's answer for - // this device (WireDepthResolveArm.h); everywhere else the order is the one B2 chose. + // a decline. m_wirePreferShaderDepthResolve is the verdict of the resolve probe that ran + // THIS pass and the shader pass on this device at bring-up (ArmWireDepthResolveOrder, + // WireDepthResolveProbe.cpp); where the probe finds the pass clean - or cannot tell - the + // order is the one B2 chose. const Bool shaderFirst = m_wirePreferShaderDepthResolve || !extensionResolve; const Bool resolvedByShader = shaderFirst && ResolveWireDepthStencilWithShader(source, resolved, source.aspect, sxMin, syMin, static_cast(width), static_cast(height)); // WHICH ARM RESOLVED IS SAID, ONCE PER ARM (review round). Lavapipe resolves correctly // through both, so the pixels cannot tell a lane which one ran; these two lines can, and - // the MsResolveQ./MsFlipQ. entries (Qualcomm's arm order substituted on the host) and the + // the MsResolveBug./MsFlipBug. entries (the probe's defect verdict forced on the host) and the // MsResolve1./MsFlip1. ones (the extension arm dropped) read them back from the server - // log - DepthStencilReadbackMatrixScenario::ExpectTheShaderResolveArmWhereTheLaneAsks. + // log - DepthStencilReadbackMatrixScenario::ExpectTheResolveArmWhereTheLaneAsks. // Keep the two phrases in step with that reader. // // AND A PREFERENCE THAT FELL BACK IS NOT SILENT: where the shader arm goes first it goes - // first because the render pass is known to leave its target unwritten on that device, + // first because the probe measured the render pass leaving its target unwritten here, // so a shader decline (a stencil aspect without VK_EXT_shader_stencil_export, a format // that is not SAMPLED_IMAGE + DEPTH_STENCIL_ATTACHMENT, a failed pipeline create - // WireMultisampleResolve.inc) that lands on the render pass is the resolve most likely @@ -763,7 +761,7 @@ MGLOG_W_ONCE("DirectVulkan::ResolveWireDepthStencil: this device prefers the shader pass, " "which did not resolve aspect 0x%x of a %u-sample format %d (stencil export=%d, " "source usage 0x%x); falling back to the no-draw VK_KHR_depth_stencil_resolve " - "render pass, which on the device that set the preference left its target " + "render pass, which this device's resolve probe measured leaving its target " "unwritten", static_cast(source.aspect), static_cast(source.samples), static_cast(source.format), m_wireShaderStencilExport ? 1 : 0, @@ -776,45 +774,16 @@ TransitionWireImage(source, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); TransitionWireImage(resolved, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); - VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; - viewInfo.image = source.image; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = source.format; - viewInfo.subresourceRange = {formatAspects, source.level, 1, source.layer, 1}; - VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &views[0])); - viewInfo.image = resolved.image; - viewInfo.subresourceRange = {formatAspects, 0, 1, 0, 1}; - VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &views[1])); - VkAttachmentDescription2 attachments[2]{}; - for (Uint32 i = 0; i < 2; ++i) { - auto& attachment = attachments[i]; - attachment.sType = VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2; - attachment.format = source.format; - attachment.samples = i == 0 ? source.samples : VK_SAMPLE_COUNT_1_BIT; - attachment.loadOp = attachment.stencilLoadOp = i == 0 ? VK_ATTACHMENT_LOAD_OP_LOAD : VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; - attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - } - VkAttachmentReference2 sourceReference{VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2}; - sourceReference.attachment = 0; - sourceReference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - sourceReference.aspectMask = formatAspects; - VkAttachmentReference2 resolveReference = sourceReference; - resolveReference.attachment = 1; - VkSubpassDescriptionDepthStencilResolve resolve{VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_DEPTH_STENCIL_RESOLVE}; - resolve.depthResolveMode = (formatAspects & VK_IMAGE_ASPECT_DEPTH_BIT) ? VK_RESOLVE_MODE_SAMPLE_ZERO_BIT : VK_RESOLVE_MODE_NONE; - resolve.stencilResolveMode = (formatAspects & VK_IMAGE_ASPECT_STENCIL_BIT) ? VK_RESOLVE_MODE_SAMPLE_ZERO_BIT : VK_RESOLVE_MODE_NONE; - resolve.pDepthStencilResolveAttachment = &resolveReference; - VkSubpassDescription2 subpass{VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_2}; - subpass.pNext = &resolve; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.pDepthStencilAttachment = &sourceReference; - VkRenderPassCreateInfo2 passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2}; - passInfo.attachmentCount = 2; - passInfo.pAttachments = attachments; - passInfo.subpassCount = 1; - passInfo.pSubpasses = &subpass; - VK_VERIFY(m_wireCreateRenderPass2(m_device, &passInfo, nullptr, &pass)); + // The view shapes and the pass are WireDepthResolveProbe.cpp's: the resolve probe runs this + // exact pass on the device at bring-up, so there is one definition of it (g5-msprobe). + const VkImageViewCreateInfo sourceViewInfo = + WireDepthStencilViewInfo(source.image, source.format, formatAspects, source.level, source.layer); + VK_VERIFY(vkCreateImageView(m_device, &sourceViewInfo, nullptr, &views[0])); + const VkImageViewCreateInfo resolvedViewInfo = + WireDepthStencilViewInfo(resolved.image, source.format, formatAspects, 0, 0); + VK_VERIFY(vkCreateImageView(m_device, &resolvedViewInfo, nullptr, &views[1])); + VK_VERIFY(CreateWireDepthStencilResolvePass(m_wireCreateRenderPass2, m_device, source.format, source.samples, + &pass)); VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; framebufferInfo.renderPass = pass; framebufferInfo.attachmentCount = 2; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.inc index b822e0f4..0d9994ca 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.inc @@ -16,8 +16,9 @@ // would make "the wire arm is observably the monolith's" untestable, and GL 4.6 core 18.3.1 // requires a SELECTED sample for stencil anyway. #if MOBILEGL_BUILD_DISAGGREGATED -#include "WireMultisampleResolveSpirv.h" - + // The pass's modules, layouts, render passes and pipeline, and the draw it records, are + // WireDepthResolveProbe.cpp's (g5-msprobe): the multisample depth/stencil resolve probe runs + // this exact arm as its CONTROL, so it is defined once for both. struct VulkanRenderer::WireMultisampleResolveResources { struct Target { VkFormat format = VK_FORMAT_UNDEFINED; @@ -25,11 +26,7 @@ VkRenderPass renderPass = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; }; - VkShaderModule vertex = VK_NULL_HANDLE; - VkShaderModule depthFragment = VK_NULL_HANDLE, stencilFragment = VK_NULL_HANDLE; - VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; - VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; - VkSampler nearest = VK_NULL_HANDLE; + WireShaderResolveKit kit; Vector targets; }; @@ -42,12 +39,7 @@ vkDestroyPipeline(m_device, target.pipeline, nullptr); vkDestroyRenderPass(m_device, target.renderPass, nullptr); } - vkDestroySampler(m_device, cache.nearest, nullptr); - vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr); - vkDestroyDescriptorSetLayout(m_device, cache.descriptorLayout, nullptr); - vkDestroyShaderModule(m_device, cache.vertex, nullptr); - vkDestroyShaderModule(m_device, cache.depthFragment, nullptr); - vkDestroyShaderModule(m_device, cache.stencilFragment, nullptr); + DestroyWireShaderResolveKit(m_device, cache.kit); delete m_wireMultisampleResolveResources; m_wireMultisampleResolveResources = nullptr; } @@ -74,57 +66,17 @@ VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT; if ((properties.optimalTilingFeatures & required) != required) return false; - VkImageAspectFlags formatAspects = VK_IMAGE_ASPECT_DEPTH_BIT; - if (source.format == VK_FORMAT_S8_UINT) formatAspects = VK_IMAGE_ASPECT_STENCIL_BIT; - else if (source.format == VK_FORMAT_D16_UNORM_S8_UINT || source.format == VK_FORMAT_D24_UNORM_S8_UINT || - source.format == VK_FORMAT_D32_SFLOAT_S8_UINT) - formatAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + const VkImageAspectFlags formatAspects = WireDepthStencilFormatAspects(source.format); if (!(formatAspects & aspect)) return false; if (!m_wireMultisampleResolveResources) { m_wireMultisampleResolveResources = new WireMultisampleResolveResources; - auto& cache = *m_wireMultisampleResolveResources; - const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) { - VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - info.codeSize = bytes; - info.pCode = code; - VK_VERIFY(vkCreateShaderModule(m_device, &info, nullptr, &module)); - }; - shader(kWireMultisampleResolveVertexSpirv, sizeof(kWireMultisampleResolveVertexSpirv), cache.vertex); - shader(kWireMultisampleDepthResolveFragmentSpirv, - sizeof(kWireMultisampleDepthResolveFragmentSpirv), cache.depthFragment); - // THE STENCIL MODULE IS ONLY CREATED WHERE IT CAN RUN: its SPIR-V declares the - // StencilExportEXT capability, and handing a module with an unsupported capability - // to vkCreateShaderModule is undefined behaviour - not an error this could catch. - if (m_wireShaderStencilExport) - shader(kWireMultisampleStencilResolveFragmentSpirv, - sizeof(kWireMultisampleStencilResolveFragmentSpirv), cache.stencilFragment); - const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; - VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - layout.bindingCount = 1; - layout.pBindings = &binding; - VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &layout, nullptr, &cache.descriptorLayout)); - // NO PUSH CONSTANT RANGE AT ALL: the pass reads its coordinate from gl_FragCoord and - // its rectangle from the scissor, so neither stage takes an input. - VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - pipelineLayout.setLayoutCount = 1; - pipelineLayout.pSetLayouts = &cache.descriptorLayout; - VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayout, nullptr, &cache.pipelineLayout)); - // NEAREST: texelFetch ignores the sampler's filter, and most devices do not - // advertise linear filtering for a depth format anyway. The sampler exists only - // because GLSL's sampler2DMS is a COMBINED image sampler. - VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; - sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; - sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = - VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST; - sampler.maxLod = 0.0f; - VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.nearest)); + VK_VERIFY(CreateWireShaderResolveKit(m_device, m_wireShaderStencilExport, + m_wireMultisampleResolveResources->kit)); } auto& cache = *m_wireMultisampleResolveResources; - const VkShaderModule fragment = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? cache.stencilFragment - : cache.depthFragment; + const VkShaderModule fragment = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? cache.kit.stencilFragment + : cache.kit.depthFragment; if (fragment == VK_NULL_HANDLE) return false; WireMultisampleResolveResources::Target* target = nullptr; @@ -135,90 +87,9 @@ target = &cache.targets.back(); target->format = resolved.format; target->aspect = aspect; - VkAttachmentDescription attachment{}; - attachment.format = resolved.format; - attachment.samples = VK_SAMPLE_COUNT_1_BIT; - // DONT_CARE on BOTH aspects: the pass writes every texel of the scissored rect for - // the aspect it owns, and the caller copies only that aspect out. The other aspect - // of a packed format is left undefined on purpose - it is never read, and LOADing - // it would order this pass behind a write nobody is waiting for. - attachment.loadOp = attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; - attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - const VkAttachmentReference depthStencil{0, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.pDepthStencilAttachment = &depthStencil; - VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; - pass.attachmentCount = 1; - pass.pAttachments = &attachment; - pass.subpassCount = 1; - pass.pSubpasses = &subpass; - VK_VERIFY(vkCreateRenderPass(m_device, &pass, nullptr, &target->renderPass)); - VkPipelineShaderStageCreateInfo stages[2]{}; - for (auto& stage : stages) { - stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - stage.pName = "main"; - } - stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; - stages[0].module = cache.vertex; - stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; - stages[1].module = fragment; - const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; - VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; - assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; - viewport.viewportCount = viewport.scissorCount = 1; - VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; - raster.polygonMode = VK_POLYGON_MODE_FILL; - raster.cullMode = VK_CULL_MODE_NONE; - raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - raster.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; - samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineDepthStencilStateCreateInfo depthStencilState{ - VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; - depthStencilState.minDepthBounds = 0.0f; - depthStencilState.maxDepthBounds = 1.0f; - if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) { - // ALWAYS + write: gl_FragDepth is the answer, and a comparison against whatever - // DONT_CARE left behind would discard some of it. - depthStencilState.depthTestEnable = VK_TRUE; - depthStencilState.depthWriteEnable = VK_TRUE; - depthStencilState.depthCompareOp = VK_COMPARE_OP_ALWAYS; - } else { - // REPLACE on every outcome, compare ALWAYS, full write mask: that is what makes - // the shader-exported reference the value that lands. The static `reference` is - // overridden by gl_FragStencilRefARB, which is the whole point of the extension. - depthStencilState.stencilTestEnable = VK_TRUE; - VkStencilOpState op{}; - op.failOp = op.passOp = op.depthFailOp = VK_STENCIL_OP_REPLACE; - op.compareOp = VK_COMPARE_OP_ALWAYS; - op.compareMask = op.writeMask = 0xFFu; - op.reference = 0; - depthStencilState.front = depthStencilState.back = op; - } - // No colour attachment in this render pass, so no attachment states either. - VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; - const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; - VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; - dynamic.dynamicStateCount = 2; - dynamic.pDynamicStates = dynamicStates; - VkGraphicsPipelineCreateInfo pipeline{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; - pipeline.stageCount = 2; - pipeline.pStages = stages; - pipeline.pVertexInputState = &vertex; - pipeline.pInputAssemblyState = &assembly; - pipeline.pViewportState = &viewport; - pipeline.pRasterizationState = &raster; - pipeline.pMultisampleState = &samples; - pipeline.pDepthStencilState = &depthStencilState; - pipeline.pColorBlendState = &blend; - pipeline.pDynamicState = &dynamic; - pipeline.layout = cache.pipelineLayout; - pipeline.renderPass = target->renderPass; - if (vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipeline, nullptr, - &target->pipeline) != VK_SUCCESS) { + VK_VERIFY(CreateWireShaderResolveRenderPass(m_device, resolved.format, &target->renderPass)); + if (CreateWireShaderResolvePipeline(m_device, cache.kit, target->renderPass, aspect, + &target->pipeline) != VK_SUCCESS) { vkDestroyRenderPass(m_device, target->renderPass, nullptr); cache.targets.pop_back(); return false; @@ -229,22 +100,19 @@ TransitionWireImage(source, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); TransitionWireImage(resolved, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); - VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; - viewInfo.image = source.image; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = source.format; // ONE ASPECT on the sampled view: a depth/stencil image can only be SAMPLED through a // view that names exactly one of its aspects, which is also what makes the stencil half // a usampler2DMS and the depth half a sampler2DMS. - viewInfo.subresourceRange = {aspect, source.level, 1, source.layer, 1}; + const VkImageViewCreateInfo sourceViewInfo = + WireDepthStencilViewInfo(source.image, source.format, aspect, source.level, source.layer); VkImageView sourceView = VK_NULL_HANDLE; - VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &sourceView)); - viewInfo.image = resolved.image; + VK_VERIFY(vkCreateImageView(m_device, &sourceViewInfo, nullptr, &sourceView)); // BOTH aspects on the attachment view: a depth/stencil ATTACHMENT names every aspect // its format has, whichever one the pipeline writes. - viewInfo.subresourceRange = {formatAspects, 0, 1, 0, 1}; + const VkImageViewCreateInfo targetViewInfo = + WireDepthStencilViewInfo(resolved.image, resolved.format, formatAspects, 0, 0); VkImageView targetView = VK_NULL_HANDLE; - VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &targetView)); + VK_VERIFY(vkCreateImageView(m_device, &targetViewInfo, nullptr, &targetView)); VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; framebufferInfo.renderPass = target->renderPass; @@ -269,9 +137,9 @@ VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; alloc.descriptorPool = descriptorPool; alloc.descriptorSetCount = 1; - alloc.pSetLayouts = &cache.descriptorLayout; + alloc.pSetLayouts = &cache.kit.descriptorLayout; VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet)); - const VkDescriptorImageInfo sampled{cache.nearest, sourceView, + const VkDescriptorImageInfo sampled{cache.kit.nearest, sourceView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; write.dstSet = descriptorSet; @@ -282,28 +150,8 @@ vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr); auto& frame = m_frameContext.GetCurrent(); - VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; - begin.renderPass = target->renderPass; - begin.framebuffer = framebuffer; - // The RECTANGLE is the render area, exactly as the extension arm's renderArea is - the - // scratch outside it is never read. - begin.renderArea = {{sx0, sy0}, {width, height}}; - vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, target->pipeline); - // THE VIEWPORT IS THE WHOLE ATTACHMENT AND THE SCISSOR IS THE RECT, and the two are not - // interchangeable here: gl_FragCoord is what the fragment shader fetches by, so the - // clip-space-to-attachment mapping has to be the identity for every rectangle. A - // viewport shrunk onto the rect would slide the whole image instead of moving a window - // over it. - const VkViewport viewport{0.0f, 0.0f, static_cast(resolved.extent.width), - static_cast(resolved.extent.height), 0.0f, 1.0f}; - const VkRect2D scissor{{sx0, sy0}, {width, height}}; - vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport); - vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor); - vkCmdBindDescriptorSets(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, - cache.pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); - vkCmdDraw(frame.commandBuffer, 3, 1, 0, 0); - vkCmdEndRenderPass(frame.commandBuffer); + RecordWireShaderResolveDraw(frame.commandBuffer, cache.kit, target->renderPass, target->pipeline, framebuffer, + descriptorSet, resolved.extent, {{sx0, sy0}, {width, height}}); ResetDynamicStateShadow(); InvalidatePipelineMemo(); // This native pass binds its own set/layout without going through the application's diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 46c26927..48450f76 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -3356,13 +3356,23 @@ if (MOBILEGL_BUILD_DISAGGREGATED) if(shaderDepthResolve AND NOT mglItestServerEnvKnobArm) continue() endif() + # P7 gate 5 (g5-msprobe): MsResolve0 on split and spawn also pins the REAL resolve probe + # on this host - it must run at the server's bring-up, find lavapipe's no-draw resolve + # render pass clean, and leave that pass first. MGITEST_EXPECT_DEPTH_RESOLVE_PROBE is a + # marker only the scenario reads (DepthStencilReadbackMatrixScenario:: + # ExpectTheResolveArmWhereTheLaneAsks), and only where the server's private log belongs + # to the entry - the tcp server is the lane fixture, so its MsResolve0 asserts pixels only. + set(msResolveProbeMarker "") + if(NOT shaderDepthResolve AND mglItestServerEnvKnobArm) + set(msResolveProbeMarker "\;MGITEST_EXPECT_DEPTH_RESOLVE_PROBE=clean") + endif() gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectVulkan.${mglItestArm}.MsResolve${shaderDepthResolve}." TEST_FILTER "DepthStencilReadbackMatrixScenario.AResolvedMultisampleDepthReadsBackFromTheDestination" DISCOVERY_TIMEOUT 30 PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} ${mglItestArmProperties} - ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=${shaderDepthResolve}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-msresolve${mglItestArmLogTag}-shader${shaderDepthResolve}.log") + ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=${shaderDepthResolve}${msResolveProbeMarker}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-msresolve${mglItestArmLogTag}-shader${shaderDepthResolve}.log") endforeach() # ---- P7 gate 5 (g5-msrbo): the SINGLE-SAMPLED depth/stencil blit ---------------------- # @@ -3405,40 +3415,41 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ${mglItestArmProperties} ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=1\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-msflip${mglItestArmLogTag}-shader1.log") endif() - # ---- P7 gate 5 (g5-msrbo review round): THE QUALCOMM ARM ORDER, ON A HOST ------------- + # ---- P7 gate 5 (g5-msrbo review round, g5-msprobe): THE DEFECT'S ARM ORDER, ON A HOST -- # # What made KHR-GL46.direct_state_access.renderbuffers_storage_multisample pass on the - # Redmi is an arm ORDER: on a Qualcomm device ResolveWireDepthStencil takes the shader - # pass first, because the Adreno's no-draw VK_KHR_depth_stencil_resolve render pass left - # its target unwritten. Lavapipe resolves correctly through both arms, so no pixel on this - # host can see the order, and without these entries dropping it went unnoticed by every - # lane. MGITEST_MAGMA_DEPTH_RESOLVE_VENDOR_ID substitutes the vendor id the server's arm - # policy reads (VulkanRenderer.cpp, next to m_wirePreferShaderDepthResolve) - nothing - # else reads it - and the two resolve cases then assert their pixels AND that the - # server's private log says the shader pass resolved and the render pass never did - # (DepthStencilReadbackMatrixScenario::ExpectTheShaderResolveArmWhereTheLaneAsks, which - # asserts the same of the MsResolve1./MsFlip1. entries above). + # Redmi is an arm ORDER: where the server's resolve probe measures the no-draw + # VK_KHR_depth_stencil_resolve render pass leaving its target unwritten (the Adreno 830's + # does) ResolveWireDepthStencil takes the shader pass first. Lavapipe resolves correctly + # through both arms - its probe verdict is clean, which MsResolve0 above pins - so no + # pixel on this host can see the order. MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=bug replaces the + # probe's MEASUREMENT with a canned "the render pass wrote nothing, the shader control + # resolved" one (VulkanRenderer::ArmWireDepthResolveOrder) - nothing else reads it - and the + # two resolve cases then assert their pixels AND that the server's private log says the + # verdict was the defect, the shader pass resolved and the render pass never did + # (DepthStencilReadbackMatrixScenario::ExpectTheResolveArmWhereTheLaneAsks, which asserts + # the same of the MsResolve1./MsFlip1. entries above). # # Split+spawn only, the server-side-knob reason above, named in spawn_lane_parity.py's # MAGMA_SERVER_ENV_KNOB_NO_TCP. # - # Red once (executed, reverted): drop `m_wirePreferShaderDepthResolve ||` from - # ResolveWireDepthStencil's `shaderFirst` (WireFramebuffer.inc) and all four entries fail - # on the arm assertion with their pixels green. + # Red once (executed, reverted): EvaluateWireDepthResolveProbe (WireDepthResolveArm.h) + # answering Clean for every measurement - the probe's detection switched off - fails all + # four entries on the arm assertions with their pixels green. if(mglItestServerEnvKnobArm) - foreach(qualcommOrderCase MsResolveQ:AResolvedMultisampleDepthReadsBackFromTheDestination - MsFlipQ:AFlippedMultisampleResolveMirrorsTheBandsAndAScaleDeclines) - string(REPLACE ":" ";" qualcommOrderPair "${qualcommOrderCase}") - list(GET qualcommOrderPair 0 qualcommOrderTail) - list(GET qualcommOrderPair 1 qualcommOrderTest) - string(TOLOWER "${qualcommOrderTail}" qualcommOrderLog) + foreach(probeBugCase MsResolveBug:AResolvedMultisampleDepthReadsBackFromTheDestination + MsFlipBug:AFlippedMultisampleResolveMirrorsTheBandsAndAScaleDeclines) + string(REPLACE ":" ";" probeBugPair "${probeBugCase}") + list(GET probeBugPair 0 probeBugTail) + list(GET probeBugPair 1 probeBugTest) + string(TOLOWER "${probeBugTail}" probeBugLog) gtest_discover_tests(MobileGLIntegrationTest - TEST_PREFIX "DirectVulkan.${mglItestArm}.${qualcommOrderTail}." - TEST_FILTER "DepthStencilReadbackMatrixScenario.${qualcommOrderTest}" + TEST_PREFIX "DirectVulkan.${mglItestArm}.${probeBugTail}." + TEST_FILTER "DepthStencilReadbackMatrixScenario.${probeBugTest}" DISCOVERY_TIMEOUT 30 PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} ${mglItestArmProperties} - ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_DEPTH_RESOLVE_VENDOR_ID=0x5143\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-${qualcommOrderLog}${mglItestArmLogTag}.log") + ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=bug\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-${probeBugLog}${mglItestArmLogTag}.log") endforeach() endif() foreach(inPlaceCase CopyImageInPlaceAcrossLevelsAndLayers diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp index d842eb54..817088a5 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp @@ -231,49 +231,72 @@ namespace MGITest { : ""); } - // P7 gate 5 (g5-msrbo review round): WHICH WIRE ARM RESOLVED, WHERE THE ENTRY SAYS - // WHICH ONE IT HAS TO BE. Lavapipe resolves a multisample depth/stencil aspect + // P7 gate 5 (g5-msrbo review round, g5-msprobe): WHICH WIRE ARM RESOLVED, WHERE THE ENTRY + // SAYS WHICH ONE IT HAS TO BE. Lavapipe resolves a multisample depth/stencil aspect // correctly through both of ResolveWireDepthStencil's arms, so the pixel assertions // above cannot tell them apart - and the fix that made // KHR-GL46.direct_state_access.renderbuffers_storage_multisample pass on the Redmi is - // exactly an arm ORDER (the shader pass first on Qualcomm, because the Adreno's no-draw - // resolve render pass leaves its target unwritten). Two kinds of entry name the arm: - // - MsResolveQ. / MsFlipQ. set MGITEST_MAGMA_DEPTH_RESOLVE_VENDOR_ID to Qualcomm's id, - // the one input the server's arm policy reads in place of the device's vendor; - // - MsResolve1. / MsFlip1. set MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=1, which - // drops the render-pass arm altogether. - // Either way the server must have resolved with the shader pass and never with the - // render pass. The claim is read off the SERVER's private log (the backend runs on the - // apply thread, the server role, on every arm), where ResolveWireDepthStencil says - // once per arm which one ran; keep the two phrases in step with WireFramebuffer.inc. - // Every other entry - the device's own vendor, whatever order that picks - asserts - // nothing here. + // exactly an arm ORDER, chosen by a resolve probe at the server's device bring-up (the + // shader pass first where the no-draw resolve render pass is measured leaving its target + // unwritten, as the Adreno 830's does). Three kinds of entry name the arm: + // - MsResolveBug. / MsFlipBug. set MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=bug, which hands + // the server's arm choice a canned "the render pass wrote nothing" measurement in + // place of running the probe: the shader pass must resolve, the render pass never; + // - MsResolve1. / MsFlip1. set MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=1, which drops + // the render-pass arm altogether: same assertion; + // - MsResolve0. on split and spawn sets MGITEST_EXPECT_DEPTH_RESOLVE_PROBE=clean - a + // marker only this function reads - and asserts the REAL probe ran on the lane's + // device (lavapipe), found the render pass clean, and that the render pass resolved. + // Without it a probe that stopped working (or started reporting lavapipe as broken) + // would leave every pixel green. + // The claim is read off the SERVER's private log (the backend runs on the apply thread, + // the server role, on every arm), where ResolveWireDepthStencil says once per arm which + // one ran and ArmWireDepthResolveOrder states the verdict; keep the phrases in step with + // WireFramebuffer.inc and VulkanRenderer.cpp. Every other entry asserts nothing here. // - // Red once (executed, reverted): drop `m_wirePreferShaderDepthResolve ||` from - // ResolveWireDepthStencil's `shaderFirst` and both Q entries fail here on split and - // spawn with the render-pass line in the log, while their pixels stay green. - static void ExpectTheShaderResolveArmWhereTheLaneAsks(const char* what) { - const std::string vendor = SplitLane::MarkerValue("MGITEST_MAGMA_DEPTH_RESOLVE_VENDOR_ID"); - const bool qualcommOrder = !vendor.empty() && std::strtoul(vendor.c_str(), nullptr, 0) == 0x5143ul; + // Red once (executed, reverted): EvaluateWireDepthResolveProbe answering Clean for every + // measurement fails the four Bug entries here (the render-pass line is in the log and + // the verdict line says clean) while their pixels stay green. + static void ExpectTheResolveArmWhereTheLaneAsks(const char* what) { + const std::string probe = SplitLane::MarkerValue("MGITEST_MAGMA_DEPTH_RESOLVE_PROBE"); + const bool forcedBug = probe == "bug"; const bool renderPassDropped = SplitLane::MarkerIsOne("MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE"); - if (!qualcommOrder && !renderPassDropped) return; + const bool measuredClean = SplitLane::MarkerValue("MGITEST_EXPECT_DEPTH_RESOLVE_PROBE") == "clean"; + if (!forcedBug && !renderPassDropped && !measuredClean) return; if (PipeStatsWindow::ServerLibraryLogPath().empty()) { - ADD_FAILURE() << what << ": the entry names the resolve arm (vendor=" << vendor - << ", force-shader=" << renderPassDropped + ADD_FAILURE() << what << ": the entry names the resolve arm (probe=" << probe + << ", force-shader=" << renderPassDropped << ", expect-clean=" << measuredClean << ") but configured no MOBILEGL_LOG_FILE_PATH, and the server's log " "is the only place the arm is visible"; return; } glFinish(); const std::string server = PipeStatsWindow::ReadServerLogSince(PipeStatsWindow::LogMark{}); - EXPECT_NE(server.find("ResolveWireDepthStencil: resolved by the shader pass"), std::string::npos) - << what << ": the server never resolved with the shader pass (vendor=" << vendor - << ", force-shader=" << renderPassDropped << ")"; - EXPECT_EQ(server.find("ResolveWireDepthStencil: resolved by the VK_KHR_depth_stencil_resolve render pass"), + const bool shaderResolved = + server.find("ResolveWireDepthStencil: resolved by the shader pass") != std::string::npos; + const bool renderPassResolved = + server.find("ResolveWireDepthStencil: resolved by the VK_KHR_depth_stencil_resolve render pass") != + std::string::npos; + if (forcedBug || renderPassDropped) { + EXPECT_TRUE(shaderResolved) << what << ": the server never resolved with the shader pass (probe=" + << probe << ", force-shader=" << renderPassDropped << ")"; + EXPECT_FALSE(renderPassResolved) + << what << ": the server resolved with the no-draw render pass, the arm the probe verdict " + "(or the knob) rules out (probe=" << probe + << ", force-shader=" << renderPassDropped << ")"; + if (forcedBug) { + EXPECT_NE(server.find("verdict=render-pass-resolve-broken"), std::string::npos) + << what << ": the forced `bug` measurement did not evaluate to the defect verdict"; + } + return; + } + EXPECT_NE(server.find("depth/stencil resolve probe (measured on this device) verdict=clean"), std::string::npos) - << what << ": the server resolved with the no-draw render pass, the arm that leaves its " - "target unwritten on the Adreno (vendor=" << vendor - << ", force-shader=" << renderPassDropped << ")"; + << what << ": the server's resolve probe did not measure this device's render pass clean"; + EXPECT_TRUE(renderPassResolved) + << what << ": a clean probe verdict must leave the render pass first, and it never resolved"; + EXPECT_FALSE(shaderResolved) << what << ": the shader pass resolved although the probe found the " + "render pass clean"; } }; @@ -446,7 +469,7 @@ namespace MGITest { ExpectAllDepth(ReadDepthFloat(0, 0, kWidth, kHeight), 0.375f, "stencil-only resolve preserves destination depth"); EXPECT_EQ(FirstGLError(), 0u); - ExpectTheShaderResolveArmWhereTheLaneAsks("depth-only and stencil-only resolves"); + ExpectTheResolveArmWhereTheLaneAsks("depth-only and stencil-only resolves"); DestroySource(resolved); DestroySource(multisampled); @@ -613,7 +636,7 @@ namespace MGITest { } // The two flipped resolves above are the only ones in this case that reach an arm: every // leg below declines on its shape first. - ExpectTheShaderResolveArmWhereTheLaneAsks("flipped depth and narrow stencil resolves"); + ExpectTheResolveArmWhereTheLaneAsks("flipped depth and narrow stencil resolves"); // A SCALE, which is INVALID_OPERATION and must leave the destination as it is. ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 3); diff --git a/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp b/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp index f479f6dd..733bb337 100644 --- a/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp +++ b/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp @@ -517,18 +517,195 @@ TEST(WirePipelineCompatibility, HashUsesCompatibilityIdentityAndSeparatesNativeD #endif } -// P7 gate 5 (g5-msrbo): the multisample depth/stencil resolve's arm order. On the Redmi (Adreno -// 830) the no-draw VK_KHR_depth_stencil_resolve render pass left its target unwritten and the -// shader arm resolved correctly, so a Qualcomm device takes the shader arm first; every other -// vendor keeps P7 wave 2-B2's order (render pass first) until a device says otherwise. -TEST(PipelineQuirkTest, WireDepthResolvePrefersTheShaderArmOnQualcommOnly) { +// P7 gate 5 (g5-msrbo, g5-msprobe): the multisample depth/stencil resolve's arm order is the +// resolve probe's VERDICT (WireDepthResolveArm.h), not the device's vendor. On the Redmi (Adreno +// 830) the no-draw VK_KHR_depth_stencil_resolve render pass left its target unwritten while the +// shader arm resolved correctly; the probe reproduces that on the actual device with the shader +// arm as its control. These tests pin every mapping from a measurement to the order through a +// FAKE RESULT SOURCE - a canned measurement handed to ChooseWireDepthResolveArm in place of the +// Vulkan probe. #if MOBILEGL_BUILD_DISAGGREGATED - using MobileGL::MG_Backend::DirectVulkan::WirePrefersShaderDepthResolve; - EXPECT_TRUE(WirePrefersShaderDepthResolve(kVendorIdQualcomm)) - << "the Adreno's no-draw resolve pass wrote nothing; the shader arm must go first there"; - EXPECT_FALSE(WirePrefersShaderDepthResolve(kVendorIdArm)); - EXPECT_FALSE(WirePrefersShaderDepthResolve(0x10005u)) << "Mesa (lavapipe) keeps the render-pass arm first"; - EXPECT_FALSE(WirePrefersShaderDepthResolve(0x10DEu)); +namespace { + using MobileGL::MG_Backend::DirectVulkan::ChooseWireDepthResolveArm; + using MobileGL::MG_Backend::DirectVulkan::DescribeWireDepthResolveProbe; + using MobileGL::MG_Backend::DirectVulkan::EvaluateWireDepthResolveProbe; + using MobileGL::MG_Backend::DirectVulkan::ParseWireDepthResolveProbeKnob; + using MobileGL::MG_Backend::DirectVulkan::WireDepthResolveAspectReading; + using MobileGL::MG_Backend::DirectVulkan::WireDepthResolveFormatReading; + using MobileGL::MG_Backend::DirectVulkan::WireDepthResolveProbeKnob; + using MobileGL::MG_Backend::DirectVulkan::WireDepthResolveProbeMeasurement; + using MobileGL::MG_Backend::DirectVulkan::WireDepthResolveProbeVerdict; + + // How one aspect's two arms read back, out of 16 texels. + struct FakeArms { + Uint32 renderPassMatches = 16; + Bool shaderRan = true; + Uint32 shaderMatches = 16; + }; + + WireDepthResolveAspectReading FakeAspect(Uint32 expected, Uint32 sentinel, FakeArms arms) { + WireDepthResolveAspectReading aspect; + aspect.measured = true; + aspect.texels = 16; + aspect.expected = expected; + aspect.sentinel = sentinel; + aspect.renderPassMatches = arms.renderPassMatches; + aspect.renderPassSentinels = 16 - arms.renderPassMatches; + aspect.renderPassFirst = arms.renderPassMatches == 16 ? expected : sentinel; + aspect.shaderRan = arms.shaderRan; + aspect.shaderMatches = arms.shaderRan ? arms.shaderMatches : 0; + aspect.shaderFirst = arms.shaderMatches == 16 ? expected : 0; + return aspect; + } + + // A packed format (both aspects) or a depth-only one (stencil = nullptr-like: not measured). + WireDepthResolveFormatReading FakeFormat(const char* name, FakeArms depth, Optional stencil) { + WireDepthResolveFormatReading reading; + reading.name = name; + reading.samples = 4; + reading.ran = true; + reading.depth = FakeAspect(0x400000u, 0xBFFFFFu, depth); + if (stencil) reading.stencil = FakeAspect(0x5Au, 0xA5u, *stencil); + return reading; + } + + WireDepthResolveProbeMeasurement FakeMeasurement(Vector formats) { + WireDepthResolveProbeMeasurement measurement; + measurement.ran = true; + measurement.formats = Move(formats); + return measurement; + } + + // The Adreno 830's reading: the render pass left the sentinel in every texel of both aspects, + // the shader control resolved all of them. + constexpr FakeArms kRenderPassWroteNothing{0, true, 16}; + constexpr FakeArms kBothArmsResolve{16, true, 16}; + // Neither arm resolved: the probe measured its own setup, or a device that cannot resolve. + constexpr FakeArms kControlFailsToo{0, true, 0}; + constexpr FakeArms kNoStencilExport{0, false, 0}; +} // namespace +#endif + +TEST(PipelineQuirkTest, WireDepthResolveProbeDetectsARenderPassThatWritesNothing) { +#if MOBILEGL_BUILD_DISAGGREGATED + Int measured = 0; + const auto choice = ChooseWireDepthResolveArm(WireDepthResolveProbeKnob::Measure, true, [&] { + ++measured; + return FakeMeasurement({FakeFormat("D24_UNORM_S8_UINT", kRenderPassWroteNothing, kRenderPassWroteNothing), + FakeFormat("D32_SFLOAT", kRenderPassWroteNothing, std::nullopt)}); + }); + EXPECT_EQ(measured, 1); + EXPECT_EQ(choice.verdict, WireDepthResolveProbeVerdict::RenderPassResolveBroken); + EXPECT_TRUE(choice.preferShader) << "the render pass writes nothing and the shader control resolves: the " + "shader arm must go first"; + // Writing WRONG values is the same defect as writing none. + auto wrong = FakeMeasurement({FakeFormat("D16_UNORM", kBothArmsResolve, std::nullopt)}); + wrong.formats[0].depth.renderPassMatches = 15; + wrong.formats[0].depth.renderPassSentinels = 0; + EXPECT_EQ(EvaluateWireDepthResolveProbe(wrong), WireDepthResolveProbeVerdict::RenderPassResolveBroken); + // One aspect of one format is enough: the render pass resolves every aspect in one pass. + EXPECT_EQ(EvaluateWireDepthResolveProbe(FakeMeasurement( + {FakeFormat("D24_UNORM_S8_UINT", kBothArmsResolve, kRenderPassWroteNothing), + FakeFormat("D32_SFLOAT", kBothArmsResolve, std::nullopt)})), + WireDepthResolveProbeVerdict::RenderPassResolveBroken); + EXPECT_NE(DescribeWireDepthResolveProbe(choice.measurement).find("render pass 0/16"), String::npos); +#else + GTEST_SKIP() << "the wire resolve arms exist only in the disaggregated build"; +#endif +} + +TEST(PipelineQuirkTest, WireDepthResolveProbeKeepsTheRenderPassFirstWhereItResolves) { +#if MOBILEGL_BUILD_DISAGGREGATED + const auto choice = ChooseWireDepthResolveArm(WireDepthResolveProbeKnob::Measure, true, [] { + return FakeMeasurement({FakeFormat("D24_UNORM_S8_UINT", kBothArmsResolve, kBothArmsResolve), + FakeFormat("D32_SFLOAT", kBothArmsResolve, std::nullopt)}); + }); + EXPECT_EQ(choice.verdict, WireDepthResolveProbeVerdict::Clean); + EXPECT_FALSE(choice.preferShader) << "lavapipe's reading: the fast arm keeps its place"; +#else + GTEST_SKIP() << "the wire resolve arms exist only in the disaggregated build"; +#endif +} + +// THE CONTROL (DriverBugProbes.h's iron rule): a render pass that failed where the shader pass +// failed too is not evidence about the render pass. +TEST(PipelineQuirkTest, WireDepthResolveProbeIsInconclusiveWhenTheShaderControlFailsToo) { +#if MOBILEGL_BUILD_DISAGGREGATED + const auto choice = ChooseWireDepthResolveArm(WireDepthResolveProbeKnob::Measure, true, [] { + return FakeMeasurement({FakeFormat("D24_UNORM_S8_UINT", kControlFailsToo, kControlFailsToo), + FakeFormat("D32_SFLOAT", kControlFailsToo, std::nullopt)}); + }); + EXPECT_EQ(choice.verdict, WireDepthResolveProbeVerdict::Inconclusive); + EXPECT_FALSE(choice.preferShader) << "an inconclusive probe must never move the arm order"; + // No control at all (no shader kit) is the same answer. + auto uncontrolled = FakeMeasurement({FakeFormat("D32_SFLOAT", kNoStencilExport, std::nullopt)}); + EXPECT_EQ(EvaluateWireDepthResolveProbe(uncontrolled), WireDepthResolveProbeVerdict::Inconclusive); + // A stencil aspect with no control (no VK_EXT_shader_stencil_export) is not counted, either + // way: its render-pass failure proves nothing, and the controlled depth aspect decides. + EXPECT_EQ(EvaluateWireDepthResolveProbe( + FakeMeasurement({FakeFormat("D24_UNORM_S8_UINT", kBothArmsResolve, kNoStencilExport)})), + WireDepthResolveProbeVerdict::Clean); + // A format whose control failed does not veto another format's conclusive defect. + EXPECT_EQ(EvaluateWireDepthResolveProbe( + FakeMeasurement({FakeFormat("D16_UNORM", kControlFailsToo, std::nullopt), + FakeFormat("D32_SFLOAT", kRenderPassWroteNothing, std::nullopt)})), + WireDepthResolveProbeVerdict::RenderPassResolveBroken); + // Skipped formats and a probe that did not run (or timed out) conclude nothing. + auto skipped = FakeMeasurement({FakeFormat("X8_D24_UNORM_PACK32", kRenderPassWroteNothing, std::nullopt)}); + skipped.formats[0].ran = false; + EXPECT_EQ(EvaluateWireDepthResolveProbe(skipped), WireDepthResolveProbeVerdict::Inconclusive); + auto timedOut = FakeMeasurement({FakeFormat("D32_SFLOAT", kRenderPassWroteNothing, std::nullopt)}); + timedOut.fenceWaitTimedOut = true; + EXPECT_EQ(EvaluateWireDepthResolveProbe(timedOut), WireDepthResolveProbeVerdict::NotRun); + EXPECT_EQ(EvaluateWireDepthResolveProbe(WireDepthResolveProbeMeasurement{}), WireDepthResolveProbeVerdict::NotRun); +#else + GTEST_SKIP() << "the wire resolve arms exist only in the disaggregated build"; +#endif +} + +TEST(PipelineQuirkTest, WireDepthResolveProbeRunsOnlyWhereThereIsARenderPassArmToJudge) { +#if MOBILEGL_BUILD_DISAGGREGATED + Int measured = 0; + const auto choice = ChooseWireDepthResolveArm(WireDepthResolveProbeKnob::Measure, false, [&] { + ++measured; + return FakeMeasurement({FakeFormat("D32_SFLOAT", kRenderPassWroteNothing, std::nullopt)}); + }); + EXPECT_EQ(measured, 0) << "without VK_KHR_depth_stencil_resolve the shader pass is the only arm"; + EXPECT_EQ(choice.verdict, WireDepthResolveProbeVerdict::NotRun); + EXPECT_FALSE(choice.preferShader); +#else + GTEST_SKIP() << "the wire resolve arms exist only in the disaggregated build"; +#endif +} + +// MGITEST_MAGMA_DEPTH_RESOLVE_PROBE replaces the MEASUREMENT, not the verdict: the canned one goes +// through the same evaluation, which is what lets the MsResolveBug./MsFlipBug. entries red when +// the evaluation stops detecting the defect. +TEST(PipelineQuirkTest, WireDepthResolveProbeKnobReplacesTheMeasurementNotTheVerdict) { +#if MOBILEGL_BUILD_DISAGGREGATED + EXPECT_EQ(ParseWireDepthResolveProbeKnob(nullptr), WireDepthResolveProbeKnob::Measure); + EXPECT_EQ(ParseWireDepthResolveProbeKnob(""), WireDepthResolveProbeKnob::Measure); + EXPECT_EQ(ParseWireDepthResolveProbeKnob("bug"), WireDepthResolveProbeKnob::ForceBug); + EXPECT_EQ(ParseWireDepthResolveProbeKnob("clean"), WireDepthResolveProbeKnob::ForceClean); + EXPECT_EQ(ParseWireDepthResolveProbeKnob("0x5143"), WireDepthResolveProbeKnob::Unrecognised); + Int measured = 0; + const auto measure = [&] { + ++measured; + return FakeMeasurement({FakeFormat("D32_SFLOAT", kBothArmsResolve, std::nullopt)}); + }; + const auto bug = ChooseWireDepthResolveArm(WireDepthResolveProbeKnob::ForceBug, true, measure); + EXPECT_TRUE(bug.measurement.fromKnob); + EXPECT_EQ(bug.verdict, WireDepthResolveProbeVerdict::RenderPassResolveBroken); + EXPECT_TRUE(bug.preferShader); + const auto clean = ChooseWireDepthResolveArm(WireDepthResolveProbeKnob::ForceClean, true, measure); + EXPECT_TRUE(clean.measurement.fromKnob); + EXPECT_EQ(clean.verdict, WireDepthResolveProbeVerdict::Clean); + EXPECT_FALSE(clean.preferShader); + EXPECT_EQ(measured, 0) << "a forced verdict must not also run the device probe"; + const auto other = ChooseWireDepthResolveArm(WireDepthResolveProbeKnob::Unrecognised, true, measure); + EXPECT_EQ(measured, 1) << "an unrecognised value runs the probe as if the knob were unset"; + EXPECT_FALSE(other.measurement.fromKnob); + EXPECT_EQ(other.verdict, WireDepthResolveProbeVerdict::Clean); #else GTEST_SKIP() << "the wire resolve arms exist only in the disaggregated build"; #endif diff --git a/scripts/ci/spawn_lane_parity.py b/scripts/ci/spawn_lane_parity.py index 1cf15c8a..ea5b6f1b 100644 --- a/scripts/ci/spawn_lane_parity.py +++ b/scripts/ci/spawn_lane_parity.py @@ -85,13 +85,14 @@ MAGMA_INPROC_ONLY = ("MagmaRunAheadScenario.", "MagmaWireCacheScenario.") # private log with the stats channel and MOBILEGL_IPC_WIRE_DEFERRED_MB=8 in the entry's environment - # server-process environment again, so the same two arms and the same tcp absence. Its three cases # are three tails (one key each) under the `.Reclaim.` prefix. -# P7 gate 5 (g5-msrbo review round): `.MsResolveQ.` / `.MsFlipQ.` set -# MGITEST_MAGMA_DEPTH_RESOLVE_VENDOR_ID, which the SERVER's depth/stencil resolve arm policy reads -# in place of the device's vendor id - the knob-free `.MsResolve0.` / `.MsFlip.` keep their tcp -# entries. +# P7 gate 5 (g5-msrbo review round, g5-msprobe): `.MsResolveBug.` / `.MsFlipBug.` set +# MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=bug, which the SERVER's depth/stencil resolve arm choice reads +# in place of running its resolve probe - the knob-free `.MsResolve0.` / `.MsFlip.` keep their tcp +# entries (MsResolve0's split/spawn copies also carry a client-read expectation marker for the +# real probe's verdict, which the tcp copy cannot: same name on all three arms, no exception). MAGMA_SERVER_ENV_KNOB_NO_TCP = (".ShaderMip1.", ".ShaderMip2.", ".DepthMip.", ".DefaultBlitShape1.", ".MsResolve1.", ".StaleSerial.", ".MsFlip1.", - ".MsResolveQ.", ".MsFlipQ.", + ".MsResolveBug.", ".MsFlipBug.", ".Reclaim.MagmaWireReclaimScenario.RespecifiesWithNoDrawBetweenKeepTheLiveStoreCountBounded", ".Reclaim.MagmaWireReclaimScenario.RespecifyAndDrawEachStoreInOneFrameStaysWithinTheDeferredBudget", ".Reclaim.MagmaWireReclaimScenario.ManySmallRespecifyAndDrawRoundsStayUnderTheStoreCountCeiling",