[Feat] (MG_Impl/GLImpl, MG_State, MG_Backend): implement glColorMaski

Promote the color writemask to per-draw-buffer state and implement the
indexed glColorMaski entry point (previously a stub), plus its read-back
through glGetBooleani_v.

- RenderState: replace the single BoolVec4 ColorMask with an array of
  MAX_DRAW_BUFFERS masks, all initialized to true. SetColorMask now
  broadcasts to every draw buffer (glColorMask semantics); GetColorMask
  returns draw buffer 0. Add indexed set/get accessors + GLContext
  wrappers.
- glColorMaski sets only the addressed draw buffer; out-of-range index
  raises GL_INVALID_VALUE (buf is a GLuint, so no GL_INVALID_ENUM path),
  mirroring the indexed blend entry points' MAX_DRAW_BUFFERS bound.
- glGetBooleani_v(GL_COLOR_WRITEMASK, i) reports draw buffer i's four
  booleans; the non-indexed glGetBooleanv still reports draw buffer 0.
- Fix GLboolean coercion in the color-mask path: any nonzero value
  enables the component (was == GL_TRUE, which wrongly rejected e.g. 2).
- DirectGLES sync reads ColorMasks[0] (GLES core has only non-indexed
  glColorMask).

Tests: ColorMaskIndexedStoresAndReadsBack covers the per-buffer vs
broadcast semantics, buffer-0 read-back, out-of-range INVALID_VALUE, and
the GLboolean coercion (mutation-verified: == GL_TRUE fails it). Full
SanityTest sweep green (30/30).
This commit is contained in:
2026-07-10 21:30:57 -04:00
parent 95876d9d8c
commit 5e8106114f
9 changed files with 145 additions and 14 deletions
+52
View File
@@ -851,3 +851,55 @@ TEST(RenderStateSanity, PolygonModeStoresAndReadsBack) {
MG_State::pGLContext.reset();
}
TEST(RenderStateSanity, ColorMaskIndexedStoresAndReadsBack) {
using namespace MobileGL;
using namespace MobileGL::MG_Impl::GLImpl;
constexpr GLuint kMaxDrawBuffers = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
// Default: every draw buffer's writemask is all-true.
GLboolean b0[4] = {GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE};
GetBooleanv(GL_COLOR_WRITEMASK, b0);
EXPECT_EQ(b0[0], GL_TRUE); EXPECT_EQ(b0[1], GL_TRUE);
EXPECT_EQ(b0[2], GL_TRUE); EXPECT_EQ(b0[3], GL_TRUE);
GLboolean bi[4] = {GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE};
GetBooleani_v(GL_COLOR_WRITEMASK, 3, bi);
EXPECT_EQ(bi[0], GL_TRUE); EXPECT_EQ(bi[3], GL_TRUE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
// glColorMaski sets ONLY the addressed draw buffer; buffer 0 stays untouched, and the non-indexed
// glGetBooleanv still reports buffer 0.
ColorMaski(2, GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GetBooleani_v(GL_COLOR_WRITEMASK, 2, bi);
EXPECT_EQ(bi[0], GL_FALSE); EXPECT_EQ(bi[1], GL_TRUE);
EXPECT_EQ(bi[2], GL_FALSE); EXPECT_EQ(bi[3], GL_TRUE);
GetBooleanv(GL_COLOR_WRITEMASK, b0);
EXPECT_EQ(b0[0], GL_TRUE); EXPECT_EQ(b0[1], GL_TRUE); // buffer 0 unchanged by ColorMaski(2, ...)
// GLboolean coercion: any nonzero byte enables the component (NOT == GL_TRUE).
ColorMaski(1, static_cast<GLboolean>(2), static_cast<GLboolean>(0),
static_cast<GLboolean>(2), static_cast<GLboolean>(0));
GetBooleani_v(GL_COLOR_WRITEMASK, 1, bi);
EXPECT_EQ(bi[0], GL_TRUE); // 2 -> TRUE
EXPECT_EQ(bi[1], GL_FALSE); // 0 -> FALSE
EXPECT_EQ(bi[2], GL_TRUE);
EXPECT_EQ(bi[3], GL_FALSE);
// glColorMask (non-indexed) broadcasts to EVERY draw buffer, overwriting the per-buffer masks.
ColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_TRUE);
GetBooleani_v(GL_COLOR_WRITEMASK, 2, bi);
EXPECT_EQ(bi[0], GL_FALSE); EXPECT_EQ(bi[2], GL_TRUE); // buffer 2 was overwritten by the broadcast
GetBooleani_v(GL_COLOR_WRITEMASK, 1, bi);
EXPECT_EQ(bi[0], GL_FALSE); EXPECT_EQ(bi[3], GL_TRUE);
// Out-of-range index -> GL_INVALID_VALUE, no state change.
ColorMaski(kMaxDrawBuffers, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
GetBooleani_v(GL_COLOR_WRITEMASK, 2, bi);
EXPECT_EQ(bi[0], GL_FALSE); // unchanged (still the broadcast value)
EXPECT_EQ(bi[2], GL_TRUE);
MG_State::pGLContext.reset();
}