[Feat] (MG_Backend/DirectVulkan): wire glPolygonMode and glColorMaski into pipeline creation

Consume the polygon mode and per-draw-buffer color write masks that the
frontend already tracks, with runtime fallback for the device features
they require.

glPolygonMode:
- Add ConvertPolygonModeToVkEnum (GL_FILL/LINE/POINT -> VkPolygonMode).
- Thread a polygonMode field through PipelineCreatePayload, fold it into
  the pipeline cache hash (distinct modes need distinct pipelines), and
  apply it in PipelineFactory instead of the hardcoded VK_POLYGON_MODE_FILL.
- LINE/POINT require the fillModeNonSolid device feature: detect and
  enable it at device creation, cache m_fillModeNonSolidFeatureEnabled,
  and fall back to FILL at pipeline-build time when it is absent.

glColorMaski:
- The per-attachment color-blend loop now reads GetColorMaskIndexed(i)
  instead of the broadcast GetColorMask(), so each draw buffer gets its
  own write mask (already covered by the pipeline hash).
- Divergent per-attachment masks require independentBlend: cache
  m_independentBlendFeatureEnabled (was enabled but never recorded) and
  fall back to draw buffer 0's mask for every attachment when it is absent.

The internal depth-mipmap utility pipeline keeps VK_POLYGON_MODE_FILL (not
GL-driven). Library builds clean; full SanityTest sweep green (30/30).
This commit is contained in:
2026-07-10 22:16:47 -04:00
parent f892f609c2
commit aa5c33a42d
6 changed files with 48 additions and 8 deletions
@@ -126,6 +126,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
@@ -217,7 +218,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
vpci.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = VK_POLYGON_MODE_FILL;
raster.polygonMode = payload.polygonMode;
raster.cullMode = payload.cullMode;
raster.frontFace = payload.frontFace;
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
@@ -29,6 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
Bool depthTestEnable = false;
@@ -2964,12 +2964,14 @@ void main() {
auto stencilTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);
const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);
const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);
auto mask = MG_State::pGLContext->GetColorMask();
const auto colorWriteMask = static_cast<VkColorComponentFlags>(
(mask.r() ? VK_COLOR_COMPONENT_R_BIT : 0u) |
(mask.g() ? VK_COLOR_COMPONENT_G_BIT : 0u) |
(mask.b() ? VK_COLOR_COMPONENT_B_BIT : 0u) |
(mask.a() ? VK_COLOR_COMPONENT_A_BIT : 0u));
const VkPolygonMode requestedPolygonMode =
MG_Util::ConvertPolygonModeToVkEnum(MG_State::pGLContext->GetPolygonModeFront());
// VK_POLYGON_MODE_LINE/_POINT require the fillModeNonSolid device feature; fall back to
// VK_POLYGON_MODE_FILL when the device lacks it.
const VkPolygonMode effectivePolygonMode =
(requestedPolygonMode == VK_POLYGON_MODE_FILL || m_fillModeNonSolidFeatureEnabled)
? requestedPolygonMode
: VK_POLYGON_MODE_FILL;
PipelineFactory::PipelineCreatePayload payload {
.programHash = programObj.hash,
@@ -2980,6 +2982,7 @@ void main() {
.rasterizationSamples = renderPassEntry.sampleCount,
.subpass = 0,
.topology = MG_Util::ConvertPrimitiveModeToVkEnum(mode),
.polygonMode = effectivePolygonMode,
.cullMode = cullFaceEnabled
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
: VK_CULL_MODE_NONE,
@@ -3075,7 +3078,16 @@ void main() {
MG_State::pGLContext->GetBlendFuncIndexed(i, srcRGB, dstRGB, srcAlpha, dstAlpha);
MG_State::pGLContext->GetBlendEquationIndexed(i, colorEquation, alphaEquation);
const Bool blendEnabled = MG_State::pGLContext->IsCapabilityEnabledIndexed(CapabilityInput::Blend, i);
VkColorComponentFlags attachmentColorWriteMask = colorWriteMask;
// Per-draw-buffer color write mask (glColorMaski). Divergent per-attachment masks require
// the independentBlend device feature; when it is absent, fall back to draw buffer 0's
// mask for every attachment (matching the non-indexed glColorMask broadcast).
const BoolVec4 bufferMask =
MG_State::pGLContext->GetColorMaskIndexed(m_independentBlendFeatureEnabled ? i : 0);
VkColorComponentFlags attachmentColorWriteMask = static_cast<VkColorComponentFlags>(
(bufferMask.r() ? VK_COLOR_COMPONENT_R_BIT : 0u) |
(bufferMask.g() ? VK_COLOR_COMPONENT_G_BIT : 0u) |
(bufferMask.b() ? VK_COLOR_COMPONENT_B_BIT : 0u) |
(bufferMask.a() ? VK_COLOR_COMPONENT_A_BIT : 0u));
Bool effectiveBlendEnabled = blendEnabled;
MG_State::GLState::ITextureObject* colorAttachmentTexture = nullptr;
if (!isDefaultDrawFbo && i < drawBuffers.size()) {
@@ -6283,6 +6295,9 @@ void main() {
VkPhysicalDeviceFeatures deviceFeatures{};
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend;
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
deviceFeatures.fillModeNonSolid = supportedDeviceFeatures.fillModeNonSolid;
m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE;
deviceFeatures.logicOp = supportedDeviceFeatures.logicOp;
deviceFeatures.shaderClipDistance = supportedDeviceFeatures.shaderClipDistance;
deviceFeatures.shaderCullDistance = supportedDeviceFeatures.shaderCullDistance;
@@ -342,6 +342,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_multiDrawIndirectFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false;
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
// drive a runtime fallback when the device lacks them.
Bool m_fillModeNonSolidFeatureEnabled = false;
Bool m_independentBlendFeatureEnabled = false;
// Cached at device creation from the graphics queue family properties
// and device limits; drives timer-query support.
Uint32 m_timestampValidBits = 0;
@@ -31,6 +31,20 @@ namespace MobileGL {
}
}
VkPolygonMode ConvertPolygonModeToVkEnum(GLenum mode) {
switch (mode) {
case GL_FILL:
return VK_POLYGON_MODE_FILL;
case GL_LINE:
return VK_POLYGON_MODE_LINE;
case GL_POINT:
return VK_POLYGON_MODE_POINT;
default:
MGLOG_W("Unrecognized polygon mode");
return VK_POLYGON_MODE_FILL;
}
}
VkCullModeFlags ConvertCullFaceModeToVkEnum(CullFaceMode v, Bool invertClockwise) {
switch (v) {
case CullFaceMode::Front:
@@ -13,6 +13,10 @@
namespace MobileGL {
namespace MG_Util {
VkPrimitiveTopology ConvertPrimitiveModeToVkEnum(GLenum mode);
// Maps GL_FILL/GL_LINE/GL_POINT to the matching VkPolygonMode. LINE/POINT require the
// fillModeNonSolid device feature; the caller is responsible for falling back to FILL when
// that feature is unavailable.
VkPolygonMode ConvertPolygonModeToVkEnum(GLenum mode);
VkCullModeFlags ConvertCullFaceModeToVkEnum(CullFaceMode value, Bool invertClockwise = false);
VkLogicOp ConvertLogicOperationToVkEnum(LogicOperation value);
VkCompareOp ConvertDepthTestFuncToVkEnum(DepthTestFunc value);