[Feat] (MG_Impl/GLImpl, MG_State): implement glHint, glPointParameter*, glPixelStoref, glGetDoublev

Six pure-state entry points that were stubs or empty // TODO bodies, all backed by new
context state and read back through glGet*.

* glHint: Hint_State was an empty TODO. Store the 4 GL 3.3 core hint targets (LINE_SMOOTH,
  POLYGON_SMOOTH, TEXTURE_COMPRESSION, FRAGMENT_SHADER_DERIVATIVE), default GL_DONT_CARE.
  Validate target and mode (FASTEST/NICEST/DONT_CARE) -> GL_INVALID_ENUM otherwise. The
  compatibility-only targets (GL_PERSPECTIVE_CORRECTION_HINT, GL_POINT_SMOOTH_HINT, GL_FOG_HINT,
  GL_GENERATE_MIPMAP_HINT) are rejected. The glGetIntegerv hint cases, previously hardcoded to
  GL_DONT_CARE, now read the stored value; glGetBooleanv on a hint is always GL_TRUE.

* glPointParameter{f,i,fv,iv}: the scalar _State bodies were empty TODOs and the *v forms were
  stubs. Only the 2 core pnames are accepted: GL_POINT_FADE_THRESHOLD_SIZE (float, default 1.0,
  GL_INVALID_VALUE if negative) and GL_POINT_SPRITE_COORD_ORIGIN (GL_LOWER_LEFT/GL_UPPER_LEFT,
  default GL_UPPER_LEFT, GL_INVALID_ENUM on a bad value -- note the different error code from the
  fade case). The compat pnames (POINT_SIZE_MIN/MAX, POINT_DISTANCE_ATTENUATION) are rejected. All
  four forms funnel through one (pname, float) handler. glGetIntegerv(GL_POINT_FADE_THRESHOLD_SIZE)
  was hardcoded to 1; it now rounds the stored float, glGetFloatv reads the float directly (keeping
  the fractional part), and GL_POINT_SPRITE_COORD_ORIGIN gained a getter case (it had none).

* glPixelStoref: funnels into the existing glPixelStorei state, but converts per type -- boolean
  pnames (PACK/UNPACK_SWAP_BYTES/LSB_FIRST) by a zero-test so 0.4 -> TRUE, integer pnames by
  round-to-nearest. A blanket round would wrongly turn a fractional true flag into false.

* glGetDoublev: funnels through glGetFloatv and widens, writing exactly the pname's component count
  (1/2/4) so a single-component query cannot overrun the caller's buffer. MobileGL stores no native
  double state (depth range/clear are float), so widening from float matches its real resolution.

State added to RenderStateParameters + RenderState Set/Get + GLContext wrappers, following the
existing LineWidth/DepthRange pattern. Covered by 4 SanityTest cases (set-then-get round trips, the
core-vs-compat enum rejections, the two different error codes, and the glPixelStoref boolean
zero-test, which was verified to fail against a blanket-round implementation).
This commit is contained in:
2026-07-10 20:35:08 -04:00
parent 561d8992bc
commit e460536119
10 changed files with 415 additions and 13 deletions
+157
View File
@@ -604,3 +604,160 @@ TEST(LogSanity, UsesEnvOverrideForFilePath) {
fs::remove(logPath);
}
// ---- Pure-state entry points: glHint / glPointParameter* / glPixelStoref / glGetDoublev -----------
TEST(RenderStateSanity, HintStoresAndReadsBack) {
using namespace MobileGL;
using namespace MobileGL::MG_Impl::GLImpl;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
// Default is GL_DONT_CARE.
GLint value = -1;
GetIntegerv(GL_LINE_SMOOTH_HINT, &value);
EXPECT_EQ(value, GL_DONT_CARE);
// Each of the 4 core targets round-trips.
Hint(GL_LINE_SMOOTH_HINT, GL_NICEST);
Hint(GL_POLYGON_SMOOTH_HINT, GL_FASTEST);
Hint(GL_TEXTURE_COMPRESSION_HINT, GL_NICEST);
Hint(GL_FRAGMENT_SHADER_DERIVATIVE_HINT, GL_FASTEST);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GetIntegerv(GL_LINE_SMOOTH_HINT, &value);
EXPECT_EQ(value, GL_NICEST);
GetIntegerv(GL_POLYGON_SMOOTH_HINT, &value);
EXPECT_EQ(value, GL_FASTEST);
GetIntegerv(GL_TEXTURE_COMPRESSION_HINT, &value);
EXPECT_EQ(value, GL_NICEST);
GetIntegerv(GL_FRAGMENT_SHADER_DERIVATIVE_HINT, &value);
EXPECT_EQ(value, GL_FASTEST);
// glGetBooleanv on a hint is always GL_TRUE (all hint enums are non-zero).
GLboolean b = GL_FALSE;
GetBooleanv(GL_LINE_SMOOTH_HINT, &b);
EXPECT_EQ(b, GL_TRUE);
// A compatibility-only target and a bad mode both raise GL_INVALID_ENUM and change nothing.
Hint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
Hint(GL_LINE_SMOOTH_HINT, GL_LINEAR);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
GetIntegerv(GL_LINE_SMOOTH_HINT, &value);
EXPECT_EQ(value, GL_NICEST); // unchanged by the failed calls
MG_State::pGLContext.reset();
}
TEST(RenderStateSanity, PointParameterStoresAndReadsBack) {
using namespace MobileGL;
using namespace MobileGL::MG_Impl::GLImpl;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
// Defaults: fade threshold 1.0, coord origin GL_UPPER_LEFT.
GLfloat f = -1.0f;
GetFloatv(GL_POINT_FADE_THRESHOLD_SIZE, &f);
EXPECT_FLOAT_EQ(f, 1.0f);
GLint origin = -1;
GetIntegerv(GL_POINT_SPRITE_COORD_ORIGIN, &origin);
EXPECT_EQ(origin, GL_UPPER_LEFT);
// Scalar float sets the fade threshold; GetFloatv keeps the fractional part, GetIntegerv rounds.
PointParameterf(GL_POINT_FADE_THRESHOLD_SIZE, 2.5f);
GetFloatv(GL_POINT_FADE_THRESHOLD_SIZE, &f);
EXPECT_FLOAT_EQ(f, 2.5f);
GLint fi = 0;
GetIntegerv(GL_POINT_FADE_THRESHOLD_SIZE, &fi);
EXPECT_EQ(fi, 3); // 2.5 rounds to nearest (to even or up; lround gives 3)
// The v-form reads params[0]; the integer form sets the coord-origin enum.
const GLint lowerLeft = GL_LOWER_LEFT;
PointParameteriv(GL_POINT_SPRITE_COORD_ORIGIN, &lowerLeft);
GetIntegerv(GL_POINT_SPRITE_COORD_ORIGIN, &origin);
EXPECT_EQ(origin, GL_LOWER_LEFT);
EXPECT_EQ(GetError(), GL_NO_ERROR);
// Errors: negative fade -> GL_INVALID_VALUE; bad coord-origin -> GL_INVALID_ENUM; compat pname ->
// GL_INVALID_ENUM. None change state.
PointParameterf(GL_POINT_FADE_THRESHOLD_SIZE, -1.0f);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
PointParameteri(GL_POINT_SPRITE_COORD_ORIGIN, GL_FASTEST);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
PointParameterf(GL_POINT_SIZE_MIN, 0.0f);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
GetFloatv(GL_POINT_FADE_THRESHOLD_SIZE, &f);
EXPECT_FLOAT_EQ(f, 2.5f); // unchanged
GetIntegerv(GL_POINT_SPRITE_COORD_ORIGIN, &origin);
EXPECT_EQ(origin, GL_LOWER_LEFT); // unchanged
MG_State::pGLContext.reset();
}
TEST(RenderStateSanity, PixelStorefRoundsAndZeroTestsBooleans) {
using namespace MobileGL;
using namespace MobileGL::MG_Impl::GLImpl;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
// Integer pname: round to nearest.
PixelStoref(GL_UNPACK_ROW_LENGTH, 7.4f);
GLint iv = 0;
GetIntegerv(GL_UNPACK_ROW_LENGTH, &iv);
EXPECT_EQ(iv, 7);
PixelStoref(GL_UNPACK_ROW_LENGTH, 7.6f);
GetIntegerv(GL_UNPACK_ROW_LENGTH, &iv);
EXPECT_EQ(iv, 8);
// Boolean pname: a fractional value must map to TRUE via a zero-test, NOT round-to-zero.
PixelStoref(GL_PACK_SWAP_BYTES, 0.4f);
GLboolean bv = GL_FALSE;
GetBooleanv(GL_PACK_SWAP_BYTES, &bv);
EXPECT_EQ(bv, GL_TRUE);
PixelStoref(GL_PACK_SWAP_BYTES, 0.0f);
GetBooleanv(GL_PACK_SWAP_BYTES, &bv);
EXPECT_EQ(bv, GL_FALSE);
// glPixelStoref matches glPixelStorei for an integer pname.
PixelStorei(GL_PACK_ALIGNMENT, 8);
GLint viaI = 0;
GetIntegerv(GL_PACK_ALIGNMENT, &viaI);
PixelStoref(GL_PACK_ALIGNMENT, 8.0f);
GLint viaF = 0;
GetIntegerv(GL_PACK_ALIGNMENT, &viaF);
EXPECT_EQ(viaI, viaF);
EXPECT_EQ(GetError(), GL_NO_ERROR);
MG_State::pGLContext.reset();
}
TEST(RenderStateSanity, GetDoublevMatchesGetFloatvWidened) {
using namespace MobileGL;
using namespace MobileGL::MG_Impl::GLImpl;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
// Single-component pname.
PointParameterf(GL_POINT_FADE_THRESHOLD_SIZE, 2.5f);
GLdouble d1[4] = {-1, -1, -1, -1};
GetDoublev(GL_POINT_FADE_THRESHOLD_SIZE, d1);
EXPECT_DOUBLE_EQ(d1[0], 2.5);
EXPECT_DOUBLE_EQ(d1[1], -1.0); // second component untouched (1-component pname)
// 2-component pname: GL_DEPTH_RANGE (default 0..1).
GLdouble d2[2] = {-1, -1};
GetDoublev(GL_DEPTH_RANGE, d2);
EXPECT_DOUBLE_EQ(d2[0], 0.0);
EXPECT_DOUBLE_EQ(d2[1], 1.0);
// 4-component pname: GL_COLOR_CLEAR_VALUE (default 0,0,0,1) matches GetFloatv widened.
GLfloat cf[4] = {};
GetFloatv(GL_COLOR_CLEAR_VALUE, cf);
GLdouble cd[4] = {};
GetDoublev(GL_COLOR_CLEAR_VALUE, cd);
for (int i = 0; i < 4; ++i) EXPECT_DOUBLE_EQ(cd[i], static_cast<GLdouble>(cf[i]));
// Null params -> GL_INVALID_VALUE.
GetDoublev(GL_DEPTH_RANGE, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
MG_State::pGLContext.reset();
}