mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix] (Review): scope the sample mask to a multisample target, version the sampled-set memo on completeness, clamp the mask word count, give the multisample placeholder every numeric domain, unwind the fixup revert one pass at a time, and bound the validator log
This commit is contained in:
@@ -447,7 +447,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
|
||||
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
|
||||
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
|
||||
ms.pSampleMask = &payload.sampleMask;
|
||||
ms.pSampleMask = payload.sampleMask;
|
||||
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
|
||||
|
||||
@@ -44,13 +44,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
|
||||
Bool sampleShadingEnable = false;
|
||||
Float minSampleShading = 0.0f;
|
||||
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask. One
|
||||
// word is the whole mask: GL_MAX_SAMPLE_MASK_WORDS is 1 on both backends, and Vulkan
|
||||
// reads ceil(rasterizationSamples / 32) words. Pipeline state like the two above -
|
||||
// Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 - so it is
|
||||
// hashed with them, and 0xffffffff (the GL default, and what a null pSampleMask
|
||||
// means) has to keep producing the pipeline it always did.
|
||||
Uint32 sampleMask = 0xffffffffu;
|
||||
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
|
||||
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
|
||||
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
|
||||
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
|
||||
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
|
||||
// pSampleMask always did.
|
||||
//
|
||||
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
|
||||
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
|
||||
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
|
||||
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
|
||||
// single Uint32 here let such a pipeline read one word past the member (the next
|
||||
// struct field). The second word is all-ones: full coverage for samples 32..63, which
|
||||
// is the only honest answer when GL has no state describing them.
|
||||
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
|
||||
Uint32 subpass = 0;
|
||||
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
Bool primitiveRestartEnable = false;
|
||||
|
||||
@@ -381,13 +381,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return used;
|
||||
}
|
||||
|
||||
// What a failed validation says, for a caller that wants to put it in its own message.
|
||||
struct SpirvValidationFailure {
|
||||
String message;
|
||||
Int result = 0;
|
||||
SizeT index = 0;
|
||||
};
|
||||
|
||||
// Returns whether the module validates. The result used to be discarded everywhere: the
|
||||
// call was DEBUG-or-env gated and only logged, so an invalid module produced by a backend
|
||||
// transform went straight to vkCreateShaderModule. That is not a survivable outcome on
|
||||
// this hardware - Mali r54 SIGSEGVs building the pipeline instead of returning an error,
|
||||
// the same "not a validating entry point" behaviour PipelineFactory already documents for
|
||||
// vkCreateGraphicsPipelines - so the one caller that feeds the driver now acts on it.
|
||||
Bool ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex) {
|
||||
// vkCreateGraphicsPipelines - so the callers that feed the driver now act on it.
|
||||
//
|
||||
// This function does NOT log the failure at E any more. It used to, unlatched, on the
|
||||
// stated grounds that "reaching here already requires the validation switch to be armed,
|
||||
// which bounds the volume" - and that premise died when the two GetOrCreateProgram call
|
||||
// sites became unconditional: MGLOG_E is live at the production INFO level, and Log.h's
|
||||
// own rule is that anything at W or E on a repeatable path must be latched or demoted.
|
||||
// The failure text now travels back through `outFailure` so the LATCHED call-site
|
||||
// messages carry the VUID instead of an unlatched inner one repeating it; what stays here
|
||||
// is the D-level detail and the process-wide counter the test lanes assert on.
|
||||
Bool ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex,
|
||||
SpirvValidationFailure* outFailure = nullptr) {
|
||||
if (outFailure != nullptr) *outFailure = {};
|
||||
if (spirv.empty()) {
|
||||
return true;
|
||||
}
|
||||
@@ -411,18 +429,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||
if (result != SPV_SUCCESS) {
|
||||
// MGLOG_E, unlatched: reaching here already requires the validation switch to
|
||||
// be armed, which bounds the volume, and each VUID names a different defect.
|
||||
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was
|
||||
// compiled out of every INFO build.) The latch is what a test harness asserts on.
|
||||
const char* message =
|
||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>";
|
||||
const SizeT index = diagnostic != nullptr ? diagnostic->position.index : 0;
|
||||
// The test-lane signal (ShaderCompiler.h documents harnesses snapshotting it and
|
||||
// asserting on the delta). Bumped for every failed validation, including one a
|
||||
// caller goes on to recover from: a transform that produced an invalid module is
|
||||
// a real defect whether or not this run survived it.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||
MGLOG_E(
|
||||
if (outFailure != nullptr) {
|
||||
*outFailure = {String(message), static_cast<Int>(result), index};
|
||||
}
|
||||
MGLOG_D(
|
||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
||||
static_cast<Int>(shaderStage),
|
||||
programExternalIndex,
|
||||
static_cast<Int>(result),
|
||||
diagnostic != nullptr ? diagnostic->position.index : 0,
|
||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>");
|
||||
index,
|
||||
message);
|
||||
}
|
||||
MOBILEGL_ASSERT(
|
||||
result == SPV_SUCCESS,
|
||||
@@ -3404,18 +3428,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// variable the link-time sanitize chain delisted from the entry-point interface
|
||||
// is invalid SPIR-V that Mali r54 turns into a SIGSEGV inside pipeline creation
|
||||
// rather than an error return. EnsureEntryPointInterface keeps them honest; this
|
||||
// is the backstop, and the one place in this function where the fallback is
|
||||
// still consistent with everything downstream, because `spv` is the input to
|
||||
// both passes and the descriptor remap has not run yet.
|
||||
// is the backstop.
|
||||
//
|
||||
// Once per program on a cache miss, and only for the single stage that carries
|
||||
// the fixups - not per draw and not per module.
|
||||
if (!ValidateTransformedSpirv(moduleSpirvs[i], stages[i], program.GetExternalIndex())) {
|
||||
// The fallback UNWINDS ONE PASS AT A TIME, which matters because the two passes
|
||||
// are not equally optional. Rewinding straight to `spv` would also throw away the
|
||||
// XfbBuffer/XfbStride/Offset decorations, the TransformFeedback capability and the
|
||||
// Xfb execution mode - while the renderer decides to call
|
||||
// vkCmdBeginTransformFeedbackEXT purely from GL state and never looks at the
|
||||
// module. That ships a pipeline whose last pre-rasterization stage has no Xfb mode
|
||||
// into a transform-feedback span, violating
|
||||
// VUID-vkCmdBeginTransformFeedbackEXT-None-04128 on exactly the driver class this
|
||||
// guard exists for. So: try the post-XFB, pre-clip-fixup module first, which keeps
|
||||
// capture working and costs only the clip-space remap.
|
||||
//
|
||||
// Once per program on a cache miss, and only for the single stage that carries the
|
||||
// fixups - not per draw and not per module.
|
||||
SpirvValidationFailure fixupFailure{};
|
||||
if (!ValidateTransformedSpirv(moduleSpirvs[i], stages[i], program.GetExternalIndex(),
|
||||
&fixupFailure)) {
|
||||
SpirvValidationFailure xfbFailure{};
|
||||
if (fixupInput != &spv &&
|
||||
ValidateTransformedSpirv(*fixupInput, stages[i], program.GetExternalIndex(), &xfbFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: the clip fixup produced an invalid module for program %u "
|
||||
"stage %d (%s); keeping the capture-decorated one, so this program draws "
|
||||
"without the clip-space remap",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
fixupFailure.message.c_str());
|
||||
moduleSpirvs[i] = *fixupInput;
|
||||
} else {
|
||||
MGLOG_E_ONCE("ProgramFactory: the clip/XFB fixups produced an invalid module for program %u "
|
||||
"stage %d; keeping the untransformed one",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]));
|
||||
"stage %d (%s); keeping the untransformed one",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
fixupFailure.message.c_str());
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
@@ -3633,10 +3680,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// introduce a reference to a delisted interface variable, the failure this whole
|
||||
// guard exists for. Anything that reaches this line names itself in the log of a
|
||||
// shipping build instead of dying anonymously inside the driver.
|
||||
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex())) {
|
||||
SpirvValidationFailure finalFailure{};
|
||||
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
|
||||
"a backend transform after the clip/XFB fixups broke it",
|
||||
"a backend transform after the clip/XFB fixups broke it (%s)",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
finalFailure.message.c_str());
|
||||
}
|
||||
|
||||
// Does the stage the driver will treat as the last pre-rasterization one actually
|
||||
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
|
||||
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
|
||||
// varying and changed nothing, anything later that might strip it. The renderer picks
|
||||
// its capture commands from GL state alone and would otherwise open a span against a
|
||||
// pipeline that cannot feed it.
|
||||
if (stages[i] == fixupStage && (flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0 &&
|
||||
!MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTransformFeedback(moduleSpv)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: program %u was built as a transform-feedback capture variant but its "
|
||||
"stage %d carries no Xfb execution mode; its capture spans will be declined rather "
|
||||
"than recorded against a pipeline that cannot feed them",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]));
|
||||
entry.xfbCaptureDeclined = true;
|
||||
}
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
@@ -4019,14 +4084,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const Vector<Uint>& spirv = binary.value().front();
|
||||
{
|
||||
// Still switch-gated, unlike the two in GetOrCreateProgram: this stage is synthesized
|
||||
// by MobileGL from a fixed template rather than transformed from application SPIR-V,
|
||||
// so a failure here is a MobileGL bug to catch in a validating lane, not something a
|
||||
// shipping build can be handed by an application. The message is latched all the same
|
||||
// - the pass-through cache is keyed on patchVertices, so a broken template would
|
||||
// otherwise re-report once per distinct patch size.
|
||||
Bool validateThisOne = false;
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
validateThisOne = true;
|
||||
#else
|
||||
if (m_enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
}
|
||||
validateThisOne = m_enableSpirvValidation;
|
||||
if (validateThisOne) MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
#endif
|
||||
SpirvValidationFailure passthroughFailure{};
|
||||
if (validateThisOne &&
|
||||
!ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0, &passthroughFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: the synthesized pass-through tessellation control stage for "
|
||||
"patchVertices=%u does not validate (%s)",
|
||||
patchVertices, passthroughFailure.message.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
smci.codeSize = spirv.size() * sizeof(Uint);
|
||||
|
||||
@@ -202,6 +202,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// tessellation stages are present or neither
|
||||
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
|
||||
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
|
||||
// True when this program was built AS a transform-feedback capture variant but its
|
||||
// last pre-rasterization module does NOT carry the Xfb execution mode - so the
|
||||
// renderer must decline the capture span instead of issuing
|
||||
// vkCmdBeginTransformFeedbackEXT against it
|
||||
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128).
|
||||
//
|
||||
// Two ways to get here, and neither is visible from GL state, which is all
|
||||
// BeginXfbCaptureForDraw otherwise consults: the clip/XFB validation backstop had to
|
||||
// rewind past the capture decoration, or XfbCaptureDecoratePass resolved none of the
|
||||
// requested varyings and returned without changing anything (its own MGLOG_E path)
|
||||
// while its runner still reported success. Both used to ship a non-Xfb module under
|
||||
// an Xfb-flagged cache entry - the flag and the layout are part of the program cache
|
||||
// key, so it was sticky for every later captured draw of the program, not a glitch.
|
||||
Bool xfbCaptureDeclined = false;
|
||||
Bool needsPassthroughTessControl = false;
|
||||
// ...and the pass-through this renderer can synthesize carries gl_Position and
|
||||
// nothing else, so it is only correct when the evaluation stage's inputs are
|
||||
|
||||
@@ -37,10 +37,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the
|
||||
// id only has to stay clear of the application's, exactly like the sampled fallback's.
|
||||
constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u;
|
||||
// The multisample sampled fallbacks. Separate ids for the same reason as the two above:
|
||||
// they must not collide with anything glGenTextures can hand out.
|
||||
constexpr Uint kFallbackTexture2DMultisampleExternalIndex = 0xFFFFFF02u;
|
||||
constexpr Uint kFallbackTexture2DMultisampleArrayExternalIndex = 0xFFFFFF03u;
|
||||
// The multisample sampled fallbacks: one per (target, numeric domain), because unlike the
|
||||
// single-sampled fallback they cannot be reinterpreted into another domain at view time
|
||||
// (see GetFallbackMultisampleTexture). Six reserved ids, contiguous from this base for the
|
||||
// same reason as the two above - they must not collide with anything glGenTextures can
|
||||
// hand out.
|
||||
constexpr Uint kFallbackMultisampleExternalIndexBase = 0xFFFFFF02u;
|
||||
constexpr Uint kFallbackMultisampleExternalIndexCount = 6u;
|
||||
|
||||
// MobileGL's own stand-in textures, by the reserved ids above. Nothing an application can
|
||||
// do reaches one, so anything keyed on the GL object an application bound - image-unit
|
||||
@@ -49,8 +52,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (texture == nullptr) return false;
|
||||
const Uint index = static_cast<Uint>(texture->GetExternalIndex());
|
||||
return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex ||
|
||||
index == kFallbackTexture2DMultisampleExternalIndex ||
|
||||
index == kFallbackTexture2DMultisampleArrayExternalIndex;
|
||||
(index >= kFallbackMultisampleExternalIndexBase &&
|
||||
index < kFallbackMultisampleExternalIndexBase + kFallbackMultisampleExternalIndexCount);
|
||||
}
|
||||
|
||||
// The R32 member of each numeric class. Every one of the three is a MANDATORY-support
|
||||
@@ -375,8 +378,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_textureManager = nullptr;
|
||||
m_samplerManager = nullptr;
|
||||
m_fallbackTexture2D.reset();
|
||||
m_fallbackTexture2DMultisample.reset();
|
||||
m_fallbackTexture2DMultisampleArray.reset();
|
||||
m_fallbackMultisampleTextures.clear();
|
||||
}
|
||||
|
||||
void UniformManager::BeginFrame(Uint32 frameIndex) {
|
||||
@@ -513,7 +515,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
texture = nullptr;
|
||||
}
|
||||
if (texture == nullptr) {
|
||||
fallbackHolder = GetFallbackTexture(preferredTarget);
|
||||
// The binding's sampler class, read here rather than through the `numericDomain`
|
||||
// local further down (it is declared after this point): the multisample placeholder
|
||||
// has to be built in the class the shader will read it in.
|
||||
fallbackHolder = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]);
|
||||
texture = fallbackHolder.get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
||||
@@ -1384,7 +1389,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return outImageInfo.imageView != VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const {
|
||||
// A multisample sampler cannot be served by the single-sampled 2D image below - its
|
||||
// descriptor demands a multisample view - so it gets its own placeholder rather than no
|
||||
// placeholder at all. Without one, ResolveSamplerDescriptor declined and
|
||||
@@ -1395,7 +1401,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// incomplete texture is undefined, not fatal, so the draw has to happen.
|
||||
if (target == TextureTarget::Texture2DMultisample ||
|
||||
target == TextureTarget::Texture2DMultisampleArray) {
|
||||
return GetFallbackMultisampleTexture(target);
|
||||
return GetFallbackMultisampleTexture(target, numericDomain);
|
||||
}
|
||||
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
|
||||
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
||||
@@ -1403,6 +1409,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The single-sampled fallback stays domain-agnostic: it is storage-image capable, so its
|
||||
// image carries VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT and ResolveSampledImageViewFormat can
|
||||
// hand an integer sampler an R8G8B8A8_UINT view of these same RGBA8 texels. A multisample
|
||||
// image can never carry that bit, which is why the arm above needs one object per domain.
|
||||
if (m_fallbackTexture2D == nullptr) {
|
||||
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
|
||||
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
@@ -1421,24 +1431,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackMultisampleTexture(
|
||||
TextureTarget target) const {
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const {
|
||||
// ONE PLACEHOLDER PER NUMERIC DOMAIN, unlike the single-sampled fallback.
|
||||
//
|
||||
// A descriptor whose image format is in a different numeric class than the sampler that
|
||||
// reads it needs a format-reinterpreting view, and building one needs
|
||||
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image. A multisample image can never have it:
|
||||
// SyncTextureResource computes storageImageCapable as `!isMultisampleTexture && ...`, and
|
||||
// the only other source of the bit is the sRGB twin, which RGBA8 is not. So an RGBA8
|
||||
// placeholder handed to a usampler2DMS made GetOrCreateSampledImageView bail with "needs
|
||||
// mutable image format", ResolveSamplerDescriptor return false, and the draw be dropped -
|
||||
// the exact outcome the placeholder exists to prevent, just reached later. Matching the
|
||||
// image's own format to the sampler's class instead means no reinterpreting view is
|
||||
// needed at all.
|
||||
const Bool arrayed = target == TextureTarget::Texture2DMultisampleArray;
|
||||
auto& slot = arrayed ? m_fallbackTexture2DMultisampleArray : m_fallbackTexture2DMultisample;
|
||||
if (slot != nullptr) {
|
||||
return slot;
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::RGBA8;
|
||||
Uint32 domainSlot = 0;
|
||||
switch (numericDomain) {
|
||||
case SamplerNumericDomain::SignedInteger:
|
||||
internalFormat = TextureInternalFormat::RGBA8I;
|
||||
domainSlot = 1;
|
||||
break;
|
||||
case SamplerNumericDomain::UnsignedInteger:
|
||||
internalFormat = TextureInternalFormat::RGBA8UI;
|
||||
domainSlot = 2;
|
||||
break;
|
||||
case SamplerNumericDomain::Float:
|
||||
case SamplerNumericDomain::Unknown:
|
||||
default:
|
||||
// Unknown reads as float, matching PlaceholderFormatForNumericDomain's own default:
|
||||
// a shader whose sampler class could not be reflected is far likelier to be a plain
|
||||
// sampler2DMS than an integer one, and a float view is the only one buildable without
|
||||
// the mutable bit anyway.
|
||||
break;
|
||||
}
|
||||
const Uint32 key = (arrayed ? kFallbackMultisampleExternalIndexCount / 2 : 0u) + domainSlot;
|
||||
auto cached = m_fallbackMultisampleTextures.find(key);
|
||||
if (cached != m_fallbackMultisampleTextures.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
const TextureUploadTarget uploadTarget = arrayed ? TextureUploadTarget::Texture2DMultisampleArray
|
||||
: TextureUploadTarget::Texture2DMultisample;
|
||||
const Uint externalIndex = kFallbackMultisampleExternalIndexBase + key;
|
||||
SharedPtr<MG_State::GLState::TextureObjectMipmap> texture;
|
||||
if (arrayed) {
|
||||
texture = MakeShared<MG_State::GLState::TextureObject2DMultisampleArray>(
|
||||
kFallbackTexture2DMultisampleArrayExternalIndex);
|
||||
texture = MakeShared<MG_State::GLState::TextureObject2DMultisampleArray>(externalIndex);
|
||||
} else {
|
||||
texture = MakeShared<MG_State::GLState::TextureObject2DMultisample>(
|
||||
kFallbackTexture2DMultisampleExternalIndex);
|
||||
texture = MakeShared<MG_State::GLState::TextureObject2DMultisample>(externalIndex);
|
||||
}
|
||||
texture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
texture->SetInternalFormat(internalFormat);
|
||||
// TWO samples, never one. VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with
|
||||
// MS = 1 from reading a VK_SAMPLE_COUNT_1_BIT image, which is exactly the hazard
|
||||
// VkTextureManager::SyncTextureResource's one-sample floor exists to avoid; a placeholder
|
||||
@@ -1453,10 +1495,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
texture->AllocateStorage(uploadTarget, 0, {.texelSize = {1, 1, 1}, .byteSize = 0});
|
||||
texture->TruncateMipmapLevels(uploadTarget, 1);
|
||||
texture->MarkStorageDirty(uploadTarget, 0, false);
|
||||
slot = texture;
|
||||
MGLOG_D("UniformManager::GetFallbackMultisampleTexture: created placeholder target=%d",
|
||||
static_cast<Int>(target));
|
||||
return slot;
|
||||
// Worth knowing if it ever fires: an integer multisample format can legitimately support
|
||||
// no count above one on a device (framebufferIntegerColorSampleCounts is allowed to be
|
||||
// VK_SAMPLE_COUNT_1_BIT), and SyncTextureResource's round-down would then hand this
|
||||
// placeholder a single-sampled image, which is the samples-08726 shape the SetSamples(2)
|
||||
// above exists to avoid. It already warns from there; nothing better is available - a
|
||||
// one-sample integer image is still a draw, and declining is the outcome this whole
|
||||
// placeholder replaced.
|
||||
MGLOG_D("UniformManager::GetFallbackMultisampleTexture: created placeholder target=%d domain=%d format=%d",
|
||||
static_cast<Int>(target), static_cast<Int>(numericDomain), static_cast<Int>(internalFormat));
|
||||
return m_fallbackMultisampleTextures.emplace(key, Move(texture)).first->second;
|
||||
}
|
||||
|
||||
VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat,
|
||||
@@ -1645,7 +1693,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// its first use instead of leaving that work to happen inside an active pass.
|
||||
// Ask GetFallbackTexture rather than re-listing the targets it serves: that list grew
|
||||
// a multisample arm and the two must not drift apart.
|
||||
texture = GetFallbackTexture(preferredTarget).get();
|
||||
texture = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]).get();
|
||||
if (texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -179,11 +179,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
||||
// The multisample arm of GetFallbackTexture. Separate object per target and no upload
|
||||
// path: a multisample image cannot be written by a transfer, so its texels stay undefined
|
||||
// - which is what GL promises for a texelFetch on an incomplete multisample texture.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(TextureTarget target) const;
|
||||
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
|
||||
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
|
||||
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
|
||||
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
|
||||
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
|
||||
// class outright rather than being reinterpreted at view time.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
|
||||
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
|
||||
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
|
||||
@@ -297,8 +303,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2DMultisample;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2DMultisampleArray;
|
||||
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
|
||||
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
|
||||
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
|
||||
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
|
||||
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
|
||||
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
|
||||
|
||||
@@ -4776,7 +4776,34 @@ void main() {
|
||||
// when the device lacks independentBlend - the same read the payload does)
|
||||
// FBO-derived payload inputs (attachment presence/formats/draw-buffer gating) are
|
||||
// pinned by the render-pass hash key, exactly as the version-keyed memo relied on.
|
||||
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount) const {
|
||||
// The fixed-function sample mask this draw actually gets, and the ONE place that decides it.
|
||||
//
|
||||
// GL 4.6 core 17.3.3 puts SAMPLE_MASK/SAMPLE_MASK_VALUE among the multisample fragment
|
||||
// operations and says they make no change "if MULTISAMPLE is disabled, or if the value of
|
||||
// SAMPLE_BUFFERS is not one" - so on a single-sample draw framebuffer the mask is a no-op.
|
||||
// Vulkan has no such rule: pSampleMask is ANDed with coverage at every rasterizationSamples,
|
||||
// and at one sample that coverage is bit 0 alone. Handing the raw GL word straight through
|
||||
// therefore turned `glEnable(GL_SAMPLE_MASK); glSampleMaski(0, 0x2);` followed by a draw to
|
||||
// the default framebuffer - the ordinary MSAA-render-then-present shape, and what dEQP's
|
||||
// multisample cases leave enabled - into a fully discarded, black draw. All-ones restores
|
||||
// the null-pSampleMask meaning the pipeline had before the mask was plumbed at all.
|
||||
//
|
||||
// SAMPLE_BUFFERS is the load-bearing half: MultisampleEnabled defaults to TRUE, so the
|
||||
// capability check alone would gate nothing. It is here for spec completeness - GL lets
|
||||
// glDisable(GL_MULTISAMPLE) switch the whole step off on a multisample target too.
|
||||
//
|
||||
// Both callers - the payload and ComputePipelineStateHash's memo word - go through this, so
|
||||
// the memo key cannot describe a different mask than the pipeline was built with.
|
||||
Uint32 VulkanRenderer::ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const {
|
||||
constexpr Uint32 kFullCoverage = 0xffffffffu;
|
||||
if (rasterizationSamples == VK_SAMPLE_COUNT_1_BIT) return kFullCoverage;
|
||||
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample)) return kFullCoverage;
|
||||
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask)) return kFullCoverage;
|
||||
return MG_State::pGLContext->GetRenderStateParameters().SampleMaskValue;
|
||||
}
|
||||
|
||||
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
||||
VkSampleCountFlagBits rasterizationSamples) const {
|
||||
// One bulk fetch instead of ~17 per-field accessor calls into MG_State: every
|
||||
// input below is a plain field of RenderStateParameters, and each accessor this
|
||||
// replaces (IsCapabilityEnabled / Get*) is a verified pure read of that same
|
||||
@@ -4795,7 +4822,13 @@ void main() {
|
||||
capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0;
|
||||
capabilityBits |= p.DepthMask ? 1ull << 8 : 0;
|
||||
capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0;
|
||||
capabilityBits |= p.SampleMaskEnabled ? 1ull << 10 : 0;
|
||||
// The EFFECTIVE mask enable, not the raw GL bit: at one sample GL says the whole
|
||||
// multisample fragment-operations step makes no change, so the pipeline is built with
|
||||
// full coverage and the memo word has to say so too. Keying on the raw bit here while
|
||||
// the payload gates on the sample count would let one FBO's cached pipeline answer for
|
||||
// another whose sample count reads the mask differently.
|
||||
const Bool sampleMaskEffective = ResolveEffectiveSampleMask(rasterizationSamples) != 0xffffffffu;
|
||||
capabilityBits |= sampleMaskEffective ? 1ull << 10 : 0;
|
||||
Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits);
|
||||
// glMinSampleShading. Hashed by BITS, not by value: this memo compares hashes rather than
|
||||
// versions, so an unhashed float would let a pipeline built at one rate be handed back
|
||||
@@ -4811,7 +4844,7 @@ void main() {
|
||||
// while GL_SAMPLE_MASK is enabled - the enable bit is already in capabilityBits, and
|
||||
// folding one more word costs nothing on a path that only recomputes when the
|
||||
// pipeline-state version moved.
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.SampleMaskValue));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(ResolveEffectiveSampleMask(rasterizationSamples)));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PatchVertices));
|
||||
// The default tessellation levels belong here for the same reason PatchVertices does:
|
||||
// when a program has an evaluation stage and no control stage, both are compiled into the
|
||||
@@ -4935,10 +4968,13 @@ void main() {
|
||||
// The version only guards recomputing the hash - unchanged version, unchanged bytes.
|
||||
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||
m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount) {
|
||||
m_pipelineStateHash = ComputePipelineStateHash(renderPassEntry.colorAttachmentCount);
|
||||
m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount ||
|
||||
m_pipelineStateHashSampleCount != renderPassEntry.sampleCount) {
|
||||
m_pipelineStateHash =
|
||||
ComputePipelineStateHash(renderPassEntry.colorAttachmentCount, renderPassEntry.sampleCount);
|
||||
m_pipelineStateHashVersion = renderStateVersion;
|
||||
m_pipelineStateHashColorCount = renderPassEntry.colorAttachmentCount;
|
||||
m_pipelineStateHashSampleCount = renderPassEntry.sampleCount;
|
||||
m_pipelineStateHashValid = true;
|
||||
}
|
||||
const Uint64 pipelineStateHash = m_pipelineStateHash;
|
||||
@@ -5207,12 +5243,8 @@ void main() {
|
||||
.sampleShadingEnable = m_sampleRateShadingFeatureEnabled &&
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading),
|
||||
.minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(),
|
||||
// GL_SAMPLE_MASK off means full coverage, which is what an all-ones mask says and what
|
||||
// a null pSampleMask used to say by omission. One word: MaxSampleMaskWords is clamped
|
||||
// to 1 on both backends, so glSampleMaski only ever writes index 0.
|
||||
.sampleMask = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask)
|
||||
? MG_State::pGLContext->GetRenderStateParameters().SampleMaskValue
|
||||
: 0xffffffffu,
|
||||
// Word 1 keeps its all-ones initialiser: GL has no state for samples 32..63.
|
||||
.sampleMask = {ResolveEffectiveSampleMask(renderPassEntry.sampleCount), 0xffffffffu},
|
||||
.subpass = 0,
|
||||
.topology = vkTopology,
|
||||
.primitiveRestartEnable = primitiveRestartEnabled,
|
||||
@@ -6075,6 +6107,10 @@ void main() {
|
||||
snap.programFactoryEpoch = m_programFactory->GetCacheStructureEpoch();
|
||||
}
|
||||
const auto& programObj = *programObjPtr;
|
||||
// Pinned for BeginXfbCaptureForDraw, which otherwise decides from GL state alone and has
|
||||
// no way to know the bound pipeline's last pre-rasterization module lost (or never got)
|
||||
// its Xfb execution mode. See VkProgramObject::xfbCaptureDeclined.
|
||||
m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined;
|
||||
|
||||
// The pipeline and the vertex-input pre-flight depend on the VAO only through
|
||||
// its resolved LAYOUT (layoutHash folds the attribute formats, bindings and the
|
||||
@@ -6237,10 +6273,13 @@ void main() {
|
||||
// instead of missing forever on a monotonic version. A miss falls through
|
||||
// to the full lookup.
|
||||
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||
m_pipelineStateHashColorCount != snap.renderPassColorCount) {
|
||||
m_pipelineStateHash = ComputePipelineStateHash(snap.renderPassColorCount);
|
||||
m_pipelineStateHashColorCount != snap.renderPassColorCount ||
|
||||
m_pipelineStateHashSampleCount != snap.renderPassSampleCount) {
|
||||
m_pipelineStateHash =
|
||||
ComputePipelineStateHash(snap.renderPassColorCount, snap.renderPassSampleCount);
|
||||
m_pipelineStateHashVersion = renderStateVersion;
|
||||
m_pipelineStateHashColorCount = snap.renderPassColorCount;
|
||||
m_pipelineStateHashSampleCount = snap.renderPassSampleCount;
|
||||
m_pipelineStateHashValid = true;
|
||||
}
|
||||
const auto memoTransformFlags =
|
||||
@@ -6475,6 +6514,10 @@ void main() {
|
||||
}
|
||||
}
|
||||
const auto& programObj = *resolvedProgramObj;
|
||||
// Pinned for BeginXfbCaptureForDraw, which otherwise decides from GL state alone and has
|
||||
// no way to know the bound pipeline's last pre-rasterization module lost (or never got)
|
||||
// its Xfb execution mode. See VkProgramObject::xfbCaptureDeclined.
|
||||
m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined;
|
||||
// For the snapshot's memoised entry pointer: if anything below inserts into the
|
||||
// program cache (blit/aux program compiles), the epoch moves and the snapshot
|
||||
// stores no pointer for this draw - the fast path then re-looks-up once.
|
||||
@@ -6513,11 +6556,31 @@ void main() {
|
||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 programVersion = program.GetBackendStateVersion();
|
||||
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
// The bind generation alone stopped covering this set the moment ResolveSampledBinding
|
||||
// started asking SamplesAsIncompleteTexture: membership now depends on the effective
|
||||
// sampler PARAMETERS (MIN_FILTER decides whether the mip chain is read at all) and on
|
||||
// the texture SHAPE, and neither moves the bind generation. A texture that flips
|
||||
// incomplete -> complete under a fixed binding - one glTexParameteri, one
|
||||
// glSamplerParameteri, a BASE_LEVEL/MAX_LEVEL change, or an upload that fills the
|
||||
// chain - would keep replaying the FALLBACK out of this memo, so the real texture
|
||||
// never got its pre-pass sync, its pending-clear materialisation or its sampled-layout
|
||||
// transition, and the descriptor path would then transition it from INSIDE the open
|
||||
// render pass, which the subpass declares no self-dependency for.
|
||||
//
|
||||
// The sampling-resolution generation is exactly the counter for that family and is
|
||||
// deliberately coarse (any texture, any sampler), so this one term covers every input
|
||||
// the predicate reads that the bind generation does not: TextureObjectBase::
|
||||
// BumpShapeVersion and SamplerObject::BumpVersion both bump it, while WHICH sampler
|
||||
// object a unit carries goes through TextureUnit::SetSamplerObject and moves the bind
|
||||
// generation instead. Same term the SetupDrawSnapshot fast path and the LOD memo
|
||||
// already carry.
|
||||
const Uint64 samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
const Bool sampledSetUnchanged =
|
||||
m_lastSampledSetValid && m_lastSampledSetProgramLifetimeId == programLifetimeId &&
|
||||
m_lastSampledSetProgramVersion == programVersion &&
|
||||
m_lastSampledSetTransformFlags == transformFlags &&
|
||||
m_lastSampledSetBindGeneration == bindGeneration;
|
||||
m_lastSampledSetBindGeneration == bindGeneration &&
|
||||
m_lastSampledSetSamplingGeneration == samplingGeneration;
|
||||
if (!sampledSetUnchanged) {
|
||||
const Bool hasSampledTextures = m_uniformManager->CollectSampledTextures(
|
||||
program, programObj, sampledTextures, &m_sampledBindingRecordsScratch);
|
||||
@@ -6527,6 +6590,7 @@ void main() {
|
||||
m_lastSampledSetProgramVersion = programVersion;
|
||||
m_lastSampledSetTransformFlags = transformFlags;
|
||||
m_lastSampledSetBindGeneration = bindGeneration;
|
||||
m_lastSampledSetSamplingGeneration = samplingGeneration;
|
||||
}
|
||||
// Complete a freshly-made LOD decision (see above): its params sum
|
||||
// can only be taken once the sampled set is known. A genuine
|
||||
@@ -6825,6 +6889,7 @@ void main() {
|
||||
snap.drawUsesDepthStencil = drawUsesDepthStencil;
|
||||
snap.renderPassExtent = renderPassEntry->extent;
|
||||
snap.renderPassColorCount = renderPassEntry->colorAttachmentCount;
|
||||
snap.renderPassSampleCount = renderPassEntry->sampleCount;
|
||||
snap.pipeline = pipeline;
|
||||
// The layout identity the fast path's aux-memo compare answers against.
|
||||
// A memo hit here, not a rebuild: the pre-flight above resolved this
|
||||
@@ -11057,6 +11122,19 @@ void main() {
|
||||
if (!program || program->GetTransformFeedbackVaryingCount() == 0) {
|
||||
return false;
|
||||
}
|
||||
// The bound pipeline's last pre-rasterization stage has to have been declared with Xfb
|
||||
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128). Everything above this line reads GL
|
||||
// state, which cannot answer that: a program can be built as a capture variant and still
|
||||
// end up with a module carrying no Xfb mode - the clip/XFB validation backstop rewinding
|
||||
// past the decoration, or XfbCaptureDecoratePass resolving none of the requested varyings
|
||||
// and changing nothing. Declining the span leaves the capture buffers untouched, which is
|
||||
// the same nothing the driver would have written, without the undefined behaviour.
|
||||
if (m_currentDrawXfbCaptureDeclined) {
|
||||
MGLOG_E_ONCE("BeginXfbCaptureForDraw: declining the capture span - the bound program's last "
|
||||
"pre-rasterization stage carries no Xfb execution mode, so recording one would be "
|
||||
"undefined behaviour rather than a capture");
|
||||
return false;
|
||||
}
|
||||
const SizeT bufferCount = std::min<SizeT>(program->GetTransformFeedbackBufferCount(), 4);
|
||||
if (bufferCount == 0) {
|
||||
return false;
|
||||
|
||||
@@ -768,9 +768,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// version: the version is monotonic and bumps on every pipeline-state
|
||||
// change, so an unchanged (version, colorAttachmentCount) proves the state
|
||||
// bytes are unchanged and the hash can be reused without re-reading them.
|
||||
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount) const;
|
||||
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
||||
VkSampleCountFlagBits rasterizationSamples) const;
|
||||
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
|
||||
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
|
||||
// and the pipeline-state memo word so the two cannot disagree.
|
||||
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
|
||||
Uint m_pipelineStateHashVersion = 0;
|
||||
Uint32 m_pipelineStateHashColorCount = 0;
|
||||
// The sample count the cached hash was computed at. A pipeline-state input now depends on
|
||||
// it (the effective sample mask), so a draw that changes only the target's sample count
|
||||
// has to recompute rather than reuse.
|
||||
VkSampleCountFlagBits m_pipelineStateHashSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
Uint64 m_pipelineStateHash = 0;
|
||||
Bool m_pipelineStateHashValid = false;
|
||||
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
|
||||
@@ -812,7 +821,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
|
||||
// texture SET is provably unchanged from the previous draw: same program (lifetime id +
|
||||
// backend-state version, which covers sampler-uniform reassignment / relink) and transform
|
||||
// flags, and no texture bind/unbind/delete since (GetTextureBindGeneration). On a hit,
|
||||
// flags, no texture bind/unbind/delete since (GetTextureBindGeneration), and nothing that
|
||||
// moves a texture's shape or a sampler's parameters since (GetSamplingResolutionGeneration
|
||||
// - membership depends on mipmap-completeness, which both of those decide). On a hit,
|
||||
// m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
|
||||
// layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
|
||||
// walk is skipped. The program lifetime id (never reused, unlike the GL name) and the
|
||||
@@ -823,6 +834,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 m_lastSampledSetProgramVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
||||
Uint64 m_lastSampledSetBindGeneration = 0;
|
||||
Uint64 m_lastSampledSetSamplingGeneration = 0;
|
||||
// Set from the draw's resolved VkProgramObject on both the full and the fast setup paths;
|
||||
// read by BeginXfbCaptureForDraw, which has only GL state otherwise. See
|
||||
// VkProgramObject::xfbCaptureDeclined.
|
||||
Bool m_currentDrawXfbCaptureDeclined = false;
|
||||
|
||||
// Memo for the per-draw explicit-LOD-0 eligibility probe
|
||||
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
|
||||
@@ -921,6 +937,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// probe the pipeline memo after a state change without re-fetching the
|
||||
// render-pass entry (the pass itself is pinned by renderPassHash above).
|
||||
Uint32 renderPassColorCount = 0;
|
||||
// Pinned with the colour count and for the same reason: the fast path recomputes the
|
||||
// pipeline-state value hash from the snapshot, and that hash reads the sample count.
|
||||
VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
// layoutHash of the snapshotting draw's vertex-input state. The pipeline and
|
||||
// the vertex-input pre-flight depend on the VAO only through this (plus the
|
||||
|
||||
@@ -59,6 +59,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/AsyncCompileScenario.cpp
|
||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||
Scenarios/UnwrittenPositionOutputScenario.cpp
|
||||
Scenarios/SampleMaskScopeScenario.cpp
|
||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||
Scenarios/SnormAttachmentScenario.cpp
|
||||
Scenarios/PipelineFailureScenario.cpp
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleMaskScopeScenario.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
|
||||
//
|
||||
// Scenario - GL_SAMPLE_MASK IS A MULTISAMPLE FRAGMENT OPERATION, SO IT DOES NOTHING AT ONE SAMPLE.
|
||||
//
|
||||
// GL 4.6 core 17.3.3 groups alpha-to-coverage, sample coverage and the sample mask together and
|
||||
// says they make no change "if MULTISAMPLE is disabled, or if the value of SAMPLE_BUFFERS is not
|
||||
// one". SAMPLE_BUFFERS is 0 for a single-sample framebuffer, so on one the mask is inert whatever
|
||||
// glSampleMaski last wrote.
|
||||
//
|
||||
// Vulkan has no such rule. VkPipelineMultisampleStateCreateInfo::pSampleMask is ANDed with
|
||||
// rasterization coverage at every rasterizationSamples, and at one sample that coverage is bit 0
|
||||
// alone - so a mask with bit 0 clear discards every fragment of every primitive. Plumbing
|
||||
// glSampleMaski straight into pSampleMask therefore turned an ordinary and legal GL sequence into
|
||||
// a fully black draw:
|
||||
//
|
||||
// glEnable(GL_SAMPLE_MASK); glSampleMaski(0, 0x2); // while an MSAA target is bound
|
||||
// ... render ...
|
||||
// glBindFramebuffer(GL_FRAMEBUFFER, 0); draw a fullscreen quad to present
|
||||
//
|
||||
// Neither piece of state is per-framebuffer, so nothing resets it when the target changes, and
|
||||
// dEQP/GL-CTS multisample cases leave exactly these masks behind. That is the MSAA-then-present
|
||||
// shape every application uses.
|
||||
//
|
||||
// The cases below are single-sample by construction (the scenario harness's colour FBO), so each
|
||||
// one asserts that the mask changed nothing.
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kFboSize = 32;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kGreenFragmentSource = R"(#version 430 core
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class SampleMaskScopeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
std::string error;
|
||||
m_program = CompileProgram(kQuadVertexSource, kGreenFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
// Process-wide GL state: leaving it set would hand the next scenario in this
|
||||
// process the very bug under test.
|
||||
glDisable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0xFFFFFFFFu);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DestroyColorFbo(m_target);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
void ExpectQuadStillPaints(const char* what) {
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << what << ": the draw raised a GL error";
|
||||
|
||||
const Image image = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(image.Empty()) << what << ": the readback came back empty";
|
||||
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0, what))
|
||||
<< what << ": an all-black target means the sample mask discarded every fragment, "
|
||||
<< "which GL says it cannot do on a single-sample framebuffer";
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The exact reported shape: bit 0 clear, so the single sample of a single-sample target is
|
||||
// masked off if the mask is applied at all.
|
||||
TEST_F(SampleMaskScopeScenario, AMaskWithBitZeroClearDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x2);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0x2");
|
||||
}
|
||||
|
||||
// Zero is the strongest form of the same thing, and the mask value the CTS's mask_zero cases
|
||||
// set.
|
||||
TEST_F(SampleMaskScopeScenario, AZeroMaskDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0");
|
||||
}
|
||||
|
||||
// Control: the same mask word with the capability disabled has never had any effect, so this
|
||||
// one passed before the fix too. It is here so a regression that ignores the enable bit
|
||||
// instead of the sample count is still caught.
|
||||
TEST_F(SampleMaskScopeScenario, ADisabledSampleMaskDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glDisable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK disabled with mask 0");
|
||||
}
|
||||
|
||||
// The mask is state, not a draw parameter, so a second draw after the first must not inherit
|
||||
// a pipeline built while the memo word and the payload disagreed. Two draws either side of a
|
||||
// mask change, both to the same single-sample target, both required to paint.
|
||||
TEST_F(SampleMaskScopeScenario, ChangingTheMaskBetweenSingleSampleDrawsKeepsBothPainting) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0xFFFFFFFFu);
|
||||
ExpectQuadStillPaints("first draw, full mask");
|
||||
glSampleMaski(0, 0x2);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "changing the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("second draw, mask 0x2");
|
||||
}
|
||||
|
||||
// GL_MAX_SAMPLE_MASK_WORDS must be 1 on both backends: MobileGL stores one word and
|
||||
// SampleMaski_State raises GL_INVALID_VALUE for any maskNumber above 0, so advertising more
|
||||
// makes dEQP's per-case gluStateReset - which issues glSampleMaski up to the advertised count
|
||||
// - fail every case. DirectGLES clamped; DirectVulkan forwarded the raw device limit.
|
||||
TEST_F(SampleMaskScopeScenario, TheAdvertisedSampleMaskWordCountMatchesWhatSampleMaskiAccepts) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
GLint words = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &words);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "querying GL_MAX_SAMPLE_MASK_WORDS raised a GL error";
|
||||
EXPECT_EQ(words, 1) << "every word below the advertised count must be writable, and only word 0 is";
|
||||
for (GLint word = 0; word < words; ++word) {
|
||||
glSampleMaski(static_cast<GLuint>(word), 0xFFFFFFFFu);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glSampleMaski(" << word << ", ...) was refused although "
|
||||
<< "GL_MAX_SAMPLE_MASK_WORDS advertises " << words << " words";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -155,6 +155,37 @@ void main() {
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
// The integer spellings of the same thing. These are the ones a plain RGBA8 multisample
|
||||
// placeholder cannot serve: a multisample image can never carry MUTABLE_FORMAT, so the
|
||||
// reinterpreting view an integer sampler would need over UNORM texels is unbuildable and
|
||||
// the descriptor resolve used to fail, losing the draw after the placeholder had already
|
||||
// been created.
|
||||
constexpr const char* kUsampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform usampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kIsampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform isampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImage2DFragmentSource = R"(#version 430 core
|
||||
@@ -454,6 +485,16 @@ void main() {
|
||||
ExpectDrawStillRuns(kSampler2DMSFragmentSource, "sampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundUnsignedSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kUsampler2DMSFragmentSource, "usampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSignedSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kIsampler2DMSFragmentSource, "isampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AFormatlessWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
|
||||
@@ -99,9 +99,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
// is deliberately left unbound so it samples as (0,0,0,1)). Deliberately coarse - ANY
|
||||
// texture, ANY sampler - so that no mutation can slip past a per-unit binding memo; the
|
||||
// setters that feed it all early-out when the value is unchanged, so the redundant
|
||||
// glTexParameteri calls applications issue every frame do not churn it. Kept separate
|
||||
// from the bind generation because the sampled texture SET is unaffected by these, and
|
||||
// the Vulkan backend's set memo keys on that one.
|
||||
// glTexParameteri calls applications issue every frame do not churn it.
|
||||
//
|
||||
// Kept separate from the bind generation because the two answer different questions, NOT
|
||||
// because the sampled texture SET is immune to this one - it is not, and the claim that
|
||||
// it was is what this comment used to say. DirectVulkan leaves an incomplete texture out
|
||||
// of the set entirely and substitutes a fallback, so completeness decides membership, and
|
||||
// its sampled-set memo carries THIS generation alongside the bind one. Any memo of a
|
||||
// resolved per-unit binding - or of which textures a draw samples at all - needs both.
|
||||
Uint64 GetSamplingResolutionGeneration() const { return m_samplingResolutionGeneration; }
|
||||
void BumpSamplingResolutionGeneration() { ++m_samplingResolutionGeneration; }
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Loader.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
@@ -177,7 +178,15 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(p.limits);
|
||||
caps.MaxIntegerSamples = MaxSampleCountFromFlags(p.limits.sampledImageIntegerSampleCounts);
|
||||
caps.MaxSamples = caps.MaxFramebufferSamples;
|
||||
caps.MaxSampleMaskWords = SaturateToInt(p.limits.maxSampleMaskWords);
|
||||
// Clamped to one word, exactly as the GLES loader clamps the driver's value and for the
|
||||
// same reason: MobileGL's sample-mask state IS a single 32-bit word
|
||||
// (RenderState::SampleMaskValue) and SampleMaski_State() raises GL_INVALID_VALUE for any
|
||||
// maskNumber other than 0. dEQP's per-case gluStateReset issues glSampleMaski up to
|
||||
// GL_MAX_SAMPLE_MASK_WORDS, so advertising a device's real 2 would abort the whole glcts
|
||||
// process after every single case - the failure da6f75dbd added the GLES clamp to stop,
|
||||
// reproduced on this backend. One word is the spec minimum and therefore always legal.
|
||||
// It is also what PipelineCreatePayload::sampleMask is sized for.
|
||||
caps.MaxSampleMaskWords = std::min(SaturateToInt(p.limits.maxSampleMaskWords), 1);
|
||||
caps.MaxTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
||||
caps.MaxVertexTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
||||
caps.MaxComputeTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
||||
@@ -300,7 +309,15 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(properties.limits);
|
||||
caps.MaxIntegerSamples = MaxSampleCountFromFlags(properties.limits.sampledImageIntegerSampleCounts);
|
||||
caps.MaxSamples = caps.MaxFramebufferSamples;
|
||||
caps.MaxSampleMaskWords = SaturateToInt(properties.limits.maxSampleMaskWords);
|
||||
// Clamped to one word, exactly as the GLES loader clamps the driver's value and for the
|
||||
// same reason: MobileGL's sample-mask state IS a single 32-bit word
|
||||
// (RenderState::SampleMaskValue) and SampleMaski_State() raises GL_INVALID_VALUE for any
|
||||
// maskNumber other than 0. dEQP's per-case gluStateReset issues glSampleMaski up to
|
||||
// GL_MAX_SAMPLE_MASK_WORDS, so advertising a device's real 2 would abort the whole glcts
|
||||
// process after every single case - the failure da6f75dbd added the GLES clamp to stop,
|
||||
// reproduced on this backend. One word is the spec minimum and therefore always legal.
|
||||
// It is also what PipelineCreatePayload::sampleMask is sized for.
|
||||
caps.MaxSampleMaskWords = std::min(SaturateToInt(properties.limits.maxSampleMaskWords), 1);
|
||||
caps.MaxTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
||||
caps.MaxVertexTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
||||
caps.MaxComputeTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
||||
|
||||
@@ -758,6 +758,33 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleDeclaresTransformFeedback(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresTransformFeedback"),
|
||||
spirv.data(), spirv.size());
|
||||
if (!context) {
|
||||
// Unparseable is not a capture verdict; say no, which makes the caller decline
|
||||
// the span rather than issue transform-feedback commands against it.
|
||||
return false;
|
||||
}
|
||||
// The exact question VUID-vkCmdBeginTransformFeedbackEXT-None-04128 asks of the
|
||||
// bound pipeline's last pre-rasterization stage: was it declared with the Xfb
|
||||
// execution mode. Reading the execution modes rather than the TransformFeedback
|
||||
// capability because the capability can legally be declared by a module that has
|
||||
// no Xfb entry point, and the VUID is about the mode.
|
||||
for (const spvtools::opt::Instruction& mode : context->module()->execution_modes()) {
|
||||
if (mode.NumInOperands() >= 2 &&
|
||||
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) ==
|
||||
spv::ExecutionMode::Xfb) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleDeclaresFloat64(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
// Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced
|
||||
|
||||
@@ -532,6 +532,12 @@ namespace MobileGL {
|
||||
// check exists so that failure can be reported as the missing capability it is,
|
||||
// naming the shader, rather than as a driver info log nobody sees.
|
||||
static Bool ModuleDeclaresBufferTextureSampler(const Vector<Uint32>& spirv);
|
||||
// Does this module carry the Xfb execution mode - i.e. would a
|
||||
// vkCmdBeginTransformFeedbackEXT against a pipeline whose last pre-rasterization
|
||||
// stage is this module satisfy VUID-vkCmdBeginTransformFeedbackEXT-None-04128?
|
||||
// Asked of the FINAL bytes, so it answers for whatever the backend transform
|
||||
// chain actually produced rather than for what it was asked to produce.
|
||||
static Bool ModuleDeclaresTransformFeedback(const Vector<Uint32>& spirv);
|
||||
|
||||
// True when the module still declares a 64-bit float type. After
|
||||
// SanitizeAndOptimizeBinary that can only mean DemoteFloat64Pass declined the
|
||||
|
||||
Reference in New Issue
Block a user