mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-16 06:23:23 +09:00
- Layout only: token streams match up to split literals, continuations and using order - The size totals fall, so the structure baseline is lowered
341 lines
18 KiB
C++
341 lines
18 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveArm.h
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
#pragma once
|
|
|
|
#include <Includes.h>
|
|
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include <utility>
|
|
|
|
// P7 gate 5 (g5-msrbo, g5-msprobe): WHICH ARM RESOLVES A MULTISAMPLE DEPTH/STENCIL ASPECT FIRST.
|
|
//
|
|
// The wire arm has two (WireFramebuffer.cpp, ResolveWireDepthStencil): a render pass that carries
|
|
// a VK_KHR_depth_stencil_resolve attachment and no draw, and the baked shader pass of
|
|
// WireMultisampleResolve.cpp, which draws. P7 wave 2-B2 took the render pass wherever the
|
|
// extension exists. On the Redmi (Adreno 830, Vulkan 1.3.284, driver 512.800.71) that pass left
|
|
// 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).
|
|
//
|
|
// 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 {
|
|
// 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;
|
|
};
|
|
|
|
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=bug|clean, not from the device.
|
|
Bool fromKnob = false;
|
|
// Measured on the device, but with MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=elide-subject: the probe
|
|
// recorded no render-pass resolve at all, so the subject's target kept its sentinel.
|
|
Bool subjectElided = false;
|
|
Vector<WireDepthResolveFormatReading> 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.
|
|
//
|
|
// A canned measurement cannot see the REAL probe - its recording, readback and tally - stop
|
|
// detecting the defect, and on lavapipe the real subject is clean, so nothing would. The third
|
|
// value, MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=elide-subject, is that negative control: the REAL
|
|
// probe runs (ChooseWireDepthResolveArm treats it as Measure) and the caller passes it on as
|
|
// WireDepthResolveProbeContext::elideSubject, so the probe records no render-pass resolve and
|
|
// the real readback keeps the sentinel while the shader control still runs. The
|
|
// DirectVulkan.{Split,Spawn}.MsResolveElide. entries then require the defect verdict from the
|
|
// measurement itself.
|
|
enum class WireDepthResolveProbeKnob : Uint8 {
|
|
Measure,
|
|
ForceBug,
|
|
ForceClean,
|
|
ElideSubject,
|
|
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;
|
|
if (std::strcmp(value, "elide-subject") == 0) return WireDepthResolveProbeKnob::ElideSubject;
|
|
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). ElideSubject does not decide: it is
|
|
// measured like Measure, and the elision is the measuring callable's to apply.
|
|
template <typename MeasureFn>
|
|
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;
|
|
}
|
|
|
|
// WHOSE VERDICT IT IS (codex closeout finding 7). The verdict is a property of ONE device and
|
|
// driver, and a process can hold more than one: inproc tears a renderer down and initializes
|
|
// another, possibly on a different physical device or under a different driver. The choice was
|
|
// a function-static - the first renderer's answer for every renderer after it - so a second
|
|
// device skipped its own probe and took an order measured elsewhere. It is now memoized per
|
|
// IDENTITY: the device (vendor, device, driver version, pipeline-cache UUID - the last changes
|
|
// with the driver build even where the version number does not) plus the two inputs the choice
|
|
// itself depends on (whether a render-pass arm exists to judge, and the test knob). The same
|
|
// identity is still probed once per process; a different one is probed for itself.
|
|
struct WireDepthResolveDeviceIdentity {
|
|
Uint32 vendorID = 0;
|
|
Uint32 deviceID = 0;
|
|
Uint32 driverVersion = 0;
|
|
Uint8 pipelineCacheUUID[16] = {};
|
|
Bool renderPassArmAvailable = false;
|
|
WireDepthResolveProbeKnob knob = WireDepthResolveProbeKnob::Measure;
|
|
|
|
Bool operator==(const WireDepthResolveDeviceIdentity& other) const {
|
|
return vendorID == other.vendorID && deviceID == other.deviceID && driverVersion == other.driverVersion &&
|
|
std::memcmp(pipelineCacheUUID, other.pipelineCacheUUID, sizeof(pipelineCacheUUID)) == 0 &&
|
|
renderPassArmAvailable == other.renderPassArmAvailable && knob == other.knob;
|
|
}
|
|
};
|
|
|
|
// The renderer's identity from its device's VkPhysicalDeviceProperties - the ONE place the fields
|
|
// are filled (VulkanRenderer::ArmWireDepthResolveOrder calls it), so the unit test proves that each
|
|
// property reaches the key: with only the cache tested, a field left out here stayed green.
|
|
inline WireDepthResolveDeviceIdentity MakeWireDepthResolveDeviceIdentity(
|
|
const VkPhysicalDeviceProperties& properties, Bool renderPassArmAvailable, WireDepthResolveProbeKnob knob) {
|
|
WireDepthResolveDeviceIdentity identity;
|
|
identity.vendorID = properties.vendorID;
|
|
identity.deviceID = properties.deviceID;
|
|
identity.driverVersion = properties.driverVersion;
|
|
static_assert(sizeof(identity.pipelineCacheUUID) == sizeof(properties.pipelineCacheUUID));
|
|
std::memcpy(identity.pipelineCacheUUID, properties.pipelineCacheUUID, sizeof(identity.pipelineCacheUUID));
|
|
identity.renderPassArmAvailable = renderPassArmAvailable;
|
|
identity.knob = knob;
|
|
return identity;
|
|
}
|
|
|
|
class WireDepthResolveArmCache {
|
|
public:
|
|
// The identity's memoized choice, or `decide()`'s - called at most once per identity, under
|
|
// the cache's lock (a probe records on the device's queue; two renderers of one identity
|
|
// must not both run it). `decided`, when given, says whether this call ran `decide`.
|
|
template <typename DecideFn>
|
|
WireDepthResolveArmChoice Resolve(const WireDepthResolveDeviceIdentity& identity, DecideFn&& decide,
|
|
Bool* decided = nullptr) {
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
for (const auto& entry : m_entries) {
|
|
if (entry.first == identity) {
|
|
if (decided) *decided = false;
|
|
return entry.second;
|
|
}
|
|
}
|
|
WireDepthResolveArmChoice choice = decide();
|
|
m_entries.emplace_back(identity, choice);
|
|
if (decided) *decided = true;
|
|
return choice;
|
|
}
|
|
|
|
SizeT Size() const {
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
return m_entries.size();
|
|
}
|
|
|
|
private:
|
|
mutable std::mutex m_mutex;
|
|
Vector<std::pair<WireDepthResolveDeviceIdentity, WireDepthResolveArmChoice>> m_entries;
|
|
};
|
|
|
|
// 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
|