[Scummvm-git-logs] scummvm master -> 27638abef614fd301e3e339947793a89594042f8

Helco noreply at scummvm.org
Thu Sep 11 15:30:08 UTC 2025


This automated email contains information about 6 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
ebc33b9abb TINYGL: Add support for texture environments
f6cf576e03 TINYGL: Add fast-path for default texture environment
3567d57211 TESTBED: Add texenv comparisons between TinyGL and OpenGL
ee25b1f3f3 TINYGL: Add TGL_CONSTANT and TGL_BLEND
27f2b1b6d6 TESTBED: Add test for TGL_BLEND
27638abef6 TETRAEDGE: Enable texture environments on TinyGL


Commit: ebc33b9abbf00be524fa72bd2025acb8cf810c70
    https://github.com/scummvm/scummvm/commit/ebc33b9abbf00be524fa72bd2025acb8cf810c70
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-11T17:30:00+02:00

Commit Message:
TINYGL: Add support for texture environments

Changed paths:
  A test/tgraphics/tinygl_texenv.h
    graphics/tinygl/gl.h
    graphics/tinygl/init.cpp
    graphics/tinygl/texelbuffer.cpp
    graphics/tinygl/texelbuffer.h
    graphics/tinygl/texture.cpp
    graphics/tinygl/zbuffer.cpp
    graphics/tinygl/zbuffer.h
    graphics/tinygl/zdirtyrect.cpp
    graphics/tinygl/zdirtyrect.h
    graphics/tinygl/zgl.h
    graphics/tinygl/ztriangle.cpp
    test/module.mk


diff --git a/graphics/tinygl/gl.h b/graphics/tinygl/gl.h
index 25aa10d8e2f..9e33c972c18 100644
--- a/graphics/tinygl/gl.h
+++ b/graphics/tinygl/gl.h
@@ -660,6 +660,23 @@ enum {
 	TGL_ALIASED_POINT_SIZE_RANGE    = 0x846D,
 
 
+	// --- GL 1.3 --- selected
+
+	// Texture environments
+	TGL_COMBINE						= 0x8570,
+	TGL_COMBINE_RGB					= 0x8571,
+	TGL_COMBINE_ALPHA				= 0x8572,
+	TGL_SOURCE0_RGB					= 0x8580,
+	TGL_SOURCE1_RGB					= 0x8581,
+	TGL_SOURCE0_ALPHA				= 0x8588,
+	TGL_SOURCE1_ALPHA				= 0x8589,
+	TGL_OPERAND0_RGB				= 0x8590,
+	TGL_OPERAND1_RGB				= 0x8591,
+	TGL_OPERAND0_ALPHA				= 0x8598,
+	TGL_OPERAND1_ALPHA				= 0x8599,
+	TGL_PRIMARY_COLOR				= 0x8577,
+	TGL_PREVIOUS					= 0x8578,
+
 	// --- GL 1.4 --- selected
 
 	// Texture mapping
diff --git a/graphics/tinygl/init.cpp b/graphics/tinygl/init.cpp
index 811b4b380d8..5ac8ef11fa3 100644
--- a/graphics/tinygl/init.cpp
+++ b/graphics/tinygl/init.cpp
@@ -161,6 +161,7 @@ void GLContext::init(int screenW, int screenH, Graphics::PixelFormat pixelFormat
 		error("glInit: texture size not allowed: %d", textureSize);
 	_textureSize = textureSize;
 	fb->setTextureSizeAndMask(textureSize, (textureSize - 1) << ZB_POINT_ST_FRAC_BITS);
+	fb->setTextureEnvironment(&_texEnv);
 
 	// allocate GLVertex array
 	vertex_max = POLYGON_MAX_VERTEX;
diff --git a/graphics/tinygl/texelbuffer.cpp b/graphics/tinygl/texelbuffer.cpp
index f9ab7daa5c0..a4c747c6a68 100644
--- a/graphics/tinygl/texelbuffer.cpp
+++ b/graphics/tinygl/texelbuffer.cpp
@@ -31,7 +31,7 @@ namespace TinyGL {
 #define ZB_POINT_ST_UNIT (1 << ZB_POINT_ST_FRAC_BITS)
 #define ZB_POINT_ST_FRAC_MASK (ZB_POINT_ST_UNIT - 1)
 
-TexelBuffer::TexelBuffer(uint width, uint height, uint textureSize) {
+TexelBuffer::TexelBuffer(uint width, uint height, uint textureSize, int internalformat) {
 	assert(width);
 	assert(height);
 	assert(textureSize);
@@ -42,6 +42,7 @@ TexelBuffer::TexelBuffer(uint width, uint height, uint textureSize) {
 	_fracTextureMask = _fracTextureUnit - 1;
 	_widthRatio = (float) width / textureSize;
 	_heightRatio = (float) height / textureSize;
+	_internalformat = internalformat;
 }
 
 static inline uint wrap(uint wrap_mode, int coord, uint _fracTextureUnit, uint _fracTextureMask) {
@@ -81,7 +82,7 @@ void TexelBuffer::getARGBAt(
 // Nearest: store texture in original size.
 class BaseNearestTexelBuffer : public TexelBuffer {
 public:
-	BaseNearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize);
+	BaseNearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize, int internalformat);
 	~BaseNearestTexelBuffer();
 
 protected:
@@ -89,7 +90,8 @@ protected:
 	Graphics::PixelFormat _format;
 };
 
-BaseNearestTexelBuffer::BaseNearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize) : TexelBuffer(width, height, textureSize), _format(format) {
+BaseNearestTexelBuffer::BaseNearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize, int internalformat)
+	: TexelBuffer(width, height, textureSize, internalformat), _format(format) {
 	uint count = _width * _height * _format.bytesPerPixel;
 	_buf = (byte *)gl_malloc(count);
 	memcpy(_buf, buf, count);
@@ -102,8 +104,8 @@ BaseNearestTexelBuffer::~BaseNearestTexelBuffer() {
 template<uint Format, uint Type>
 class NearestTexelBuffer final : public BaseNearestTexelBuffer {
 public:
-	NearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize)
-	  : BaseNearestTexelBuffer(buf, format, width, height, textureSize) {}
+	NearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize, int internalformat)
+	  : BaseNearestTexelBuffer(buf, format, width, height, textureSize, internalformat) {}
 
 protected:
 	void getARGBAt(
@@ -122,8 +124,8 @@ protected:
 template<>
 class NearestTexelBuffer<TGL_RGB, TGL_UNSIGNED_BYTE> final : public BaseNearestTexelBuffer {
 public:
-	NearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize)
-	  : BaseNearestTexelBuffer(buf, format, width, height, textureSize) {}
+	NearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize, int internalformat)
+	  : BaseNearestTexelBuffer(buf, format, width, height, textureSize, internalformat) {}
 
 protected:
 	void getARGBAt(
@@ -139,36 +141,41 @@ protected:
 	}
 };
 
-TexelBuffer *createNearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &pf, uint format, uint type, uint width, uint height, uint textureSize) {
+TexelBuffer *createNearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &pf, uint format, uint type, uint width, uint height, uint textureSize, int internalformat) {
 	if (format == TGL_RGBA && type == TGL_UNSIGNED_BYTE) {
 		return new NearestTexelBuffer<TGL_RGBA, TGL_UNSIGNED_BYTE>(
 			buf, pf,
 			width, height,
-			textureSize
+			textureSize,
+			internalformat
 		);
 	} else if (format == TGL_RGB && type == TGL_UNSIGNED_BYTE) {
 		return new NearestTexelBuffer<TGL_RGB,  TGL_UNSIGNED_BYTE>(
 			buf, pf,
 			width, height,
-			textureSize
+			textureSize,
+			internalformat
 		);
 	} else if (format == TGL_RGB && type == TGL_UNSIGNED_SHORT_5_6_5) {
 		return new NearestTexelBuffer<TGL_RGB,  TGL_UNSIGNED_SHORT_5_6_5>(
 			buf, pf,
 			width, height,
-			textureSize
+			textureSize,
+			internalformat
 		);
 	} else if (format == TGL_RGBA && type == TGL_UNSIGNED_SHORT_5_5_5_1) {
 		return new NearestTexelBuffer<TGL_RGBA, TGL_UNSIGNED_SHORT_5_5_5_1>(
 			buf, pf,
 			width, height,
-			textureSize
+			textureSize,
+			internalformat
 		);
 	} else if (format == TGL_RGBA && type == TGL_UNSIGNED_SHORT_4_4_4_4) {
 		return new NearestTexelBuffer<TGL_RGBA, TGL_UNSIGNED_SHORT_4_4_4_4>(
 			buf, pf,
 			width, height,
-			textureSize
+			textureSize,
+			internalformat
 		);
 	} else {
 		error("TinyGL texture: format 0x%04x and type 0x%04x combination not supported", format, type);
@@ -183,7 +190,7 @@ TexelBuffer *createNearestTexelBuffer(const byte *buf, const Graphics::PixelForm
 // usage increase should be negligible.
 class BilinearTexelBuffer : public TexelBuffer {
 public:
-	BilinearTexelBuffer(byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize);
+	BilinearTexelBuffer(byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSiz, int internalformat);
 	~BilinearTexelBuffer();
 
 protected:
@@ -207,7 +214,8 @@ private:
 #define P11_OFFSET 3
 #define PIXEL_PER_TEXEL_SHIFT 2
 
-BilinearTexelBuffer::BilinearTexelBuffer(byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize) : TexelBuffer(width, height, textureSize) {
+BilinearTexelBuffer::BilinearTexelBuffer(byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize, int internalformat)
+	: TexelBuffer(width, height, textureSize, internalformat) {
 	const Graphics::PixelBuffer src(format, buf);
 
 	uint pixel00_offset = 0, pixel11_offset, pixel01_offset, pixel10_offset;
@@ -319,11 +327,12 @@ void BilinearTexelBuffer::getARGBAt(
 	);
 }
 
-TexelBuffer *createBilinearTexelBuffer(byte *buf, const Graphics::PixelFormat &pf, uint format, uint type, uint width, uint height, uint textureSize) {
+TexelBuffer *createBilinearTexelBuffer(byte *buf, const Graphics::PixelFormat &pf, uint format, uint type, uint width, uint height, uint textureSize, int internalformat) {
 	return new BilinearTexelBuffer(
 		buf, pf,
 		width, height,
-		textureSize
+		textureSize,
+		internalformat
 	);
 }
 
diff --git a/graphics/tinygl/texelbuffer.h b/graphics/tinygl/texelbuffer.h
index 596e95c40a2..cde77458422 100644
--- a/graphics/tinygl/texelbuffer.h
+++ b/graphics/tinygl/texelbuffer.h
@@ -28,9 +28,11 @@ namespace TinyGL {
 
 class TexelBuffer {
 public:
-	TexelBuffer(uint width, uint height, uint textureSize);
+	TexelBuffer(uint width, uint height, uint textureSize, int internalformat);
 	virtual ~TexelBuffer() {};
 
+	inline int internalformat() const { return _internalformat; }
+
 	void getARGBAt(
 		uint wrap_s, uint wrap_t,
 		int s, int t,
@@ -45,10 +47,11 @@ protected:
 	) const = 0;
 	uint _width, _height, _fracTextureUnit, _fracTextureMask;
 	float _widthRatio, _heightRatio;
+	int _internalformat;
 };
 
-TexelBuffer *createNearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &pf, uint format, uint type, uint width, uint height, uint textureSize);
-TexelBuffer *createBilinearTexelBuffer(byte *buf, const Graphics::PixelFormat &pf, uint format, uint type, uint width, uint height, uint textureSize);
+TexelBuffer *createNearestTexelBuffer(const byte *buf, const Graphics::PixelFormat &pf, uint format, uint type, uint width, uint height, uint textureSize, int internalformat);
+TexelBuffer *createBilinearTexelBuffer(byte *buf, const Graphics::PixelFormat &pf, uint format, uint type, uint width, uint height, uint textureSize, int internalformat);
 
 } // end of namespace TinyGL
 
diff --git a/graphics/tinygl/texture.cpp b/graphics/tinygl/texture.cpp
index ee5315c30b7..3be2cba08ca 100644
--- a/graphics/tinygl/texture.cpp
+++ b/graphics/tinygl/texture.cpp
@@ -165,7 +165,8 @@ void GLContext::glopTexImage2D(GLParam *p) {
 				pixels, pf,
 				format, type,
 				width, height,
-				_textureSize
+				_textureSize,
+				internalformat
 			);
 			break;
 		default:
@@ -173,7 +174,8 @@ void GLContext::glopTexImage2D(GLParam *p) {
 				pixels, pf,
 				format, type,
 				width, height,
-				_textureSize
+				_textureSize,
+				internalformat
 			);
 			break;
 		}
@@ -191,11 +193,82 @@ error:
 		error("tglTexParameter: unsupported option");
 	}
 
-	if (pname != TGL_TEXTURE_ENV_MODE)
-		goto error;
-
-	if (param != TGL_DECAL)
+	switch (pname) {
+	case TGL_TEXTURE_ENV_MODE:
+		if (param == TGL_REPLACE ||
+			param == TGL_MODULATE ||
+			param == TGL_DECAL ||
+			//param == TGL_BLEND || // no tex env constants yet
+			param == TGL_ADD ||
+			param == TGL_COMBINE)
+			_texEnv.envMode = param;
+		else
+			goto error;
+		break;
+	case TGL_COMBINE_RGB:
+		if (param == TGL_REPLACE ||
+			param == TGL_MODULATE ||
+			param == TGL_ADD)
+			_texEnv.combineRGB = param;
+		else
+			goto error;
+		break;
+	case TGL_COMBINE_ALPHA:
+		if (param == TGL_REPLACE ||
+			param == TGL_MODULATE ||
+			param == TGL_ADD)
+			_texEnv.combineAlpha = param;
+		else
+			goto error;
+		break;
+	case TGL_SOURCE0_RGB:
+	case TGL_SOURCE1_RGB:
+	{
+		GLTextureEnvArgument *op = pname == TGL_SOURCE0_RGB ? &_texEnv.arg0 : &_texEnv.arg1;
+		if (param == TGL_TEXTURE ||
+			param == TGL_PRIMARY_COLOR)
+			op->sourceRGB = param;
+		else
+			goto error;
+		break;
+	}
+	case TGL_SOURCE0_ALPHA:
+	case TGL_SOURCE1_ALPHA:
+	{
+		GLTextureEnvArgument *op = pname == TGL_SOURCE0_ALPHA ? &_texEnv.arg0 : &_texEnv.arg1;
+		if (param == TGL_TEXTURE ||
+			param == TGL_PRIMARY_COLOR)
+			op->sourceAlpha = param;
+		else
+			goto error;
+		break;
+	}
+	case TGL_OPERAND0_RGB:
+	case TGL_OPERAND1_RGB:
+	{
+		GLTextureEnvArgument *op = pname == TGL_OPERAND0_RGB ? &_texEnv.arg0 : &_texEnv.arg1;
+		if (param == TGL_SRC_COLOR ||
+			param == TGL_ONE_MINUS_SRC_COLOR ||
+			param == TGL_SRC_ALPHA)
+			op->operandRGB = param;
+		else
+			goto error;
+		break;
+	}
+	case TGL_OPERAND0_ALPHA:
+	case TGL_OPERAND1_ALPHA:
+	{
+		GLTextureEnvArgument *op = pname == TGL_OPERAND0_ALPHA ? &_texEnv.arg0 : &_texEnv.arg1;
+		if (param == TGL_SRC_ALPHA ||
+			param == TGL_ONE_MINUS_SRC_ALPHA)
+			op->operandAlpha = param;
+		else
+			goto error;
+		break;
+	}
+	default:
 		goto error;
+	}
 }
 
 // TODO: not all tests are done
diff --git a/graphics/tinygl/zbuffer.cpp b/graphics/tinygl/zbuffer.cpp
index b2fe9fff620..0fc653d284c 100644
--- a/graphics/tinygl/zbuffer.cpp
+++ b/graphics/tinygl/zbuffer.cpp
@@ -279,4 +279,184 @@ Graphics::Surface *copyFromFrameBuffer(const Graphics::PixelFormat &dstFormat) {
 	return c->fb->copyFromFrameBuffer(dstFormat);
 }
 
+void FrameBuffer::applyTextureEnvironment(
+	int internalformat,
+	uint previousA, uint previousR, uint previousG, uint previousB,
+	byte &texA, byte &texR, byte &texG, byte &texB)
+{
+	// summary notation is used from https://registry.khronos.org/OpenGL-Refpages/gl2.1/xhtml/glTexEnv.xml
+	// previousARGB is still in 16bit fixed-point format
+	// texARGB is both input and output
+	// GL_RGB/GL_RGBA might be identical as TexelBuffer returns As=1
+
+	const auto satAdd = [](byte a, byte b) -> byte {
+		// from: https://web.archive.org/web/20190213215419/https://locklessinc.com/articles/sat_arithmetic/
+		byte r = a + b;
+		return (byte)(r | -(r < a));
+	};
+
+	const auto sat16_to_8 = [](uint32 x) -> byte {
+		x = (x + 128) >> 8; // rounding 16 to 8 
+		return (byte)(x | -!!(x >> 8)); // branchfree saturation
+	};
+
+	const auto fpMul = [](byte a, byte b) -> byte {
+		// from: https://community.khronos.org/t/precision-curiosity-1-255-or-1-256/40539/11
+		// correct would be (a*b)/255 but that is slow, instead we use (a*b) * 257/256 / 256
+		// this also implicitly saturates
+		uint32 r = a * b;
+		return (byte)((r + (r >> 8) + 127) >> 8);
+	};
+
+	struct Arg {
+		byte a, r, g, b;
+	};
+	const auto getCombineArg = [&](const GLTextureEnvArgument &mode) -> Arg {
+		Arg op = {}, opColor = {};
+
+		// Source values
+		switch (mode.sourceRGB) {
+		case TGL_TEXTURE:
+			opColor.a = texA;
+			opColor.r = texR;
+			opColor.g = texG;
+			opColor.b = texB;
+			break;
+		case TGL_PRIMARY_COLOR:
+			opColor.a = sat16_to_8(previousA);
+			opColor.r = sat16_to_8(previousR);
+			opColor.g = sat16_to_8(previousG);
+			opColor.b = sat16_to_8(previousB);
+			break;
+		default:
+			assert(false && "Invalid texture environment arg color source");
+			break;
+		}
+		switch (mode.sourceAlpha) {
+		case TGL_TEXTURE:
+			op.a = texA;
+			break;
+		case TGL_PRIMARY_COLOR:
+			op.a = sat16_to_8(previousA);
+			break;
+		default:
+			assert(false && "Invalid texture environment arg alpha source");
+			break;
+		}
+
+		// Operands
+		switch (mode.operandRGB) {
+		case TGL_SRC_COLOR:
+			op.r = opColor.r; // intermediate values were necessary for operandRGB == TGL_SRC_ALPHA 
+			op.g = opColor.g;
+			op.b = opColor.b;
+			break;
+		case TGL_ONE_MINUS_SRC_COLOR:
+			op.r = 255 - opColor.r;
+			op.g = 255 - opColor.g;
+			op.b = 255 - opColor.b;
+			break;
+		case TGL_SRC_ALPHA:
+			op.r = op.g = op.b = opColor.a;
+			break;
+		default:
+			assert(false && "Invalid texture environment arg color operand");
+			break;
+		}
+		switch (mode.operandAlpha) {
+		case TGL_SRC_ALPHA:
+			break;
+		case TGL_ONE_MINUS_SRC_ALPHA:
+			op.a = 255 - op.a;
+			break;
+		default:
+			assert(false && "Invalid texture environment arg alpha operand");
+			break;
+		}
+
+		return op;
+	};
+
+	switch (_textureEnv->envMode) {
+	case TGL_REPLACE:
+		// GL_RGB:  Cs | Ap
+		// GL_RGBA: Cs | As
+		texA = internalformat == TGL_RGBA ? texA : sat16_to_8(previousA);
+		break;
+	case TGL_MODULATE:
+	{
+		// GL_RGB:  CpCs | Ap 
+		// GL_RGBA: CpCs | ApAs
+		texA = fpMul(sat16_to_8(previousA), texA);
+		texR = fpMul(sat16_to_8(previousR), texR);
+		texG = fpMul(sat16_to_8(previousG), texG);
+		texB = fpMul(sat16_to_8(previousB), texB);
+		break;
+	}
+	case TGL_DECAL:
+	{
+		// GL_RGB:  Cs              | Ap
+		// GL_RGBA: Cp(1-As) + CsAs | Ap
+		texR = satAdd(fpMul(sat16_to_8(previousR), 255 - texA), fpMul(texR, texA));
+		texG = satAdd(fpMul(sat16_to_8(previousG), 255 - texA), fpMul(texG, texA));
+		texB = satAdd(fpMul(sat16_to_8(previousB), 255 - texA), fpMul(texB, texA));
+		texA = sat16_to_8(previousA);
+		break;
+	}
+	case TGL_ADD:
+	{
+		// GL_RGB: Cp + Cs | Ap
+		// GL_RGB: Cp + Cs | ApAs
+		texA = fpMul(sat16_to_8(previousA), texA);
+		texR = satAdd(sat16_to_8(previousR), texR);
+		texG = satAdd(sat16_to_8(previousG), texG);
+		texB = satAdd(sat16_to_8(previousB), texB);
+		break;
+	}
+	case TGL_COMBINE:
+	{
+		Arg arg0 = getCombineArg(_textureEnv->arg0);
+		Arg arg1 = getCombineArg(_textureEnv->arg1);
+		switch (_textureEnv->combineRGB) {
+		case TGL_REPLACE:
+			texR = arg0.r;
+			texG = arg0.g;
+			texB = arg0.b;
+			break;
+		case TGL_MODULATE:
+			texR = fpMul(arg0.r, arg1.r);
+			texG = fpMul(arg0.g, arg1.g);
+			texB = fpMul(arg0.b, arg1.b);
+			break;
+		case TGL_ADD:
+			texR = satAdd(arg0.r, arg1.r);
+			texG = satAdd(arg0.g, arg1.g);
+			texB = satAdd(arg0.b, arg1.b);
+			break;
+		default:
+			assert(false && "Invalid texture environment color combine");
+			break;
+		}
+		
+		switch (_textureEnv->combineAlpha) {
+		case TGL_REPLACE:
+			texA = arg0.a;
+			break;
+		case TGL_MODULATE:
+			texA = fpMul(arg0.a, arg1.a);
+			break;
+		case TGL_ADD:
+			texA = satAdd(arg0.a, arg1.a);
+			break;
+		default:
+			assert(false && "Invalid texture environment alpha combine");
+		}
+		break;
+	}
+	default:
+		assert(false && "Invalid texture environment mode");
+		break;
+	}
+}
+
 } // end of namespace TinyGL
diff --git a/graphics/tinygl/zbuffer.h b/graphics/tinygl/zbuffer.h
index e44950377dc..109a6bff8e2 100644
--- a/graphics/tinygl/zbuffer.h
+++ b/graphics/tinygl/zbuffer.h
@@ -76,6 +76,8 @@ static const int DRAW_DEPTH_ONLY = 0;
 static const int DRAW_FLAT = 1;
 static const int DRAW_SMOOTH = 2;
 
+struct GLTextureEnv; // defined in zgl.h
+
 struct Buffer {
 	byte *pbuf;
 	uint *zbuf;
@@ -673,6 +675,10 @@ public:
 		_wrapT = wrapt;
 	}
 
+	void setTextureEnvironment(const GLTextureEnv *texEnv) {
+		_textureEnv = texEnv;
+	}
+
 	void setTextureSizeAndMask(int textureSize, int textureSizeMask) {
 		_textureSize = textureSize;
 		_textureSizeMask = textureSizeMask;
@@ -773,6 +779,11 @@ private:
 	template <bool kInterpRGB, bool kInterpZ, bool kDepthWrite, bool kEnableScissor>
 	void drawLine(const ZBufferPoint *p1, const ZBufferPoint *p2);
 
+	void applyTextureEnvironment(
+		int internalformat,
+		uint previousA, uint previousR, uint previousG, uint previousB,
+		byte &texA, byte &texR, byte &texG, byte &texB);
+
 	Buffer _offscreenBuffer;
 	byte *_pbuf;
 	int _pbufWidth;
@@ -792,6 +803,7 @@ private:
 	bool _clippingEnabled;
 
 	const TexelBuffer *_currentTexture;
+	const GLTextureEnv *_textureEnv;
 	uint _wrapS, _wrapT;
 	bool _blendingEnabled;
 	int _sourceBlendingFactor;
diff --git a/graphics/tinygl/zdirtyrect.cpp b/graphics/tinygl/zdirtyrect.cpp
index 495f51994f1..7a14a426a0e 100644
--- a/graphics/tinygl/zdirtyrect.cpp
+++ b/graphics/tinygl/zdirtyrect.cpp
@@ -27,6 +27,21 @@
 
 namespace TinyGL {
 
+GLTextureEnvArgument::GLTextureEnvArgument()
+	: sourceRGB(TGL_TEXTURE)
+	, operandRGB(TGL_SRC_COLOR)
+	, sourceAlpha(TGL_TEXTURE)
+	, operandAlpha(TGL_SRC_ALPHA) {}
+
+GLTextureEnv::GLTextureEnv()
+	: envMode(TGL_MODULATE)
+	, combineRGB(TGL_REPLACE)
+	, combineAlpha(TGL_REPLACE) {}
+
+bool GLTextureEnv::isDefault() const {
+	return envMode == TGL_MODULATE;
+}
+
 void GLContext::issueDrawCall(DrawCall *drawCall) {
 	if (_enableDirtyRectangles && drawCall->getDirtyRegion().isEmpty())
 		return;
@@ -469,6 +484,7 @@ RasterizationDrawCall::RasterizationState RasterizationDrawCall::captureState()
 	state.texture = c->current_texture;
 	state.wrapS = c->texture_wrap_s;
 	state.wrapT = c->texture_wrap_t;
+	state.textureEnv = c->_texEnv;
 	state.lightingEnabled = c->lighting_enabled;
 	state.textureVersion = c->current_texture->versionNumber;
 	state.fogEnabled = c->fog_enabled;
@@ -543,6 +559,7 @@ void RasterizationDrawCall::applyState(const RasterizationDrawCall::Rasterizatio
 	c->current_texture = state.texture;
 	c->texture_wrap_s = state.wrapS;
 	c->texture_wrap_t = state.wrapT;
+	c->_texEnv = state.textureEnv;
 	c->fog_enabled = state.fogEnabled;
 	c->fog_color = Vector4(state.fogColorR, state.fogColorG, state.fogColorB, 1.0f);
 
diff --git a/graphics/tinygl/zdirtyrect.h b/graphics/tinygl/zdirtyrect.h
index 07396ad3550..569049dd53a 100644
--- a/graphics/tinygl/zdirtyrect.h
+++ b/graphics/tinygl/zdirtyrect.h
@@ -38,6 +38,24 @@ struct GLContext;
 struct GLVertex;
 struct GLTexture;
 
+struct GLTextureEnvArgument {
+	GLTextureEnvArgument();
+
+	uint
+		sourceRGB,
+		operandRGB,
+		sourceAlpha,
+		operandAlpha;
+};
+
+struct GLTextureEnv {
+	GLTextureEnv();
+	bool isDefault() const;
+
+	uint envMode, combineRGB, combineAlpha;
+	GLTextureEnvArgument arg0, arg1;
+};
+
 class DrawCall {
 public:
 
@@ -159,6 +177,7 @@ private:
 		byte polygonStipplePattern[128];
 		GLTexture *texture;
 		uint wrapS, wrapT;
+		GLTextureEnv textureEnv;
 		bool fogEnabled;
 		float fogColorR;
 		float fogColorG;
diff --git a/graphics/tinygl/zgl.h b/graphics/tinygl/zgl.h
index e19934c9081..638e8a25431 100644
--- a/graphics/tinygl/zgl.h
+++ b/graphics/tinygl/zgl.h
@@ -324,6 +324,7 @@ struct GLContext {
 	int texture_min_filter;
 	uint texture_wrap_s;
 	uint texture_wrap_t;
+	GLTextureEnv _texEnv;
 	Common::Array<struct tglColorAssociation> colorAssociationList;
 
 	// shared state
diff --git a/graphics/tinygl/ztriangle.cpp b/graphics/tinygl/ztriangle.cpp
index 30741a82116..e4bb9216e61 100644
--- a/graphics/tinygl/ztriangle.cpp
+++ b/graphics/tinygl/ztriangle.cpp
@@ -123,14 +123,12 @@ void FrameBuffer::putPixelTexture(int fbOffset, const TexelBuffer *texture,
 		uint8 c_a, c_r, c_g, c_b;
 		texture->getARGBAt(wrap_s, wrap_t, s, t, c_a, c_r, c_g, c_b);
 		if (kLightsMode) {
-			uint l_a = (a >> (ZB_POINT_ALPHA_BITS - 8));
-			uint l_r = (r >> (ZB_POINT_RED_BITS - 8));
-			uint l_g = (g >> (ZB_POINT_GREEN_BITS - 8));
-			uint l_b = (b >> (ZB_POINT_BLUE_BITS - 8));
-			c_a = (c_a * l_a) >> (ZB_POINT_ALPHA_BITS - 8);
-			c_r = (c_r * l_r) >> (ZB_POINT_RED_BITS - 8);
-			c_g = (c_g * l_g) >> (ZB_POINT_GREEN_BITS - 8);
-			c_b = (c_b * l_b) >> (ZB_POINT_BLUE_BITS - 8);
+			// the name kLightsMode might be misleading, it is only false for
+			// depth-only triangles, in which case: sure, no texture env needed
+			applyTextureEnvironment(
+				texture->internalformat(),
+				a, r, g, b,
+				c_a, c_r, c_g, c_b);
 		}
 		writePixel<kEnableAlphaTest, kEnableBlending, kDepthWrite, kFogMode>(fbOffset + _a, c_a, c_r, c_g, c_b, z, fog, fog_r, fog_g, fog_b);
 	}
diff --git a/test/module.mk b/test/module.mk
index 2c82c1c5216..48926845f2b 100644
--- a/test/module.mk
+++ b/test/module.mk
@@ -28,6 +28,10 @@ TEST_LIBS += test/null_osystem.o \
 	backends/platform/sdl/win32/win32_wrapper.o
 endif
 
+ifdef USE_TINYGL
+TESTS += $(srcdir)/test/tgraphics/tinygl*.h
+endif
+
 TEST_LIBS +=	audio/libaudio.a math/libmath.a common/formats/libformats.a common/compression/libcompression.a common/libcommon.a image/libimage.a graphics/libgraphics.a
 
 ifeq ($(ENABLE_WINTERMUTE), STATIC_PLUGIN)
diff --git a/test/tgraphics/tinygl_texenv.h b/test/tgraphics/tinygl_texenv.h
new file mode 100644
index 00000000000..9d08bdd5c7f
--- /dev/null
+++ b/test/tgraphics/tinygl_texenv.h
@@ -0,0 +1,182 @@
+#include <cxxtest/TestSuite.h>
+
+#ifdef USE_TINYGL
+
+#include "graphics/tinygl/tinygl.h"
+
+// every test sets up some texture environment
+// then draws a single pixel and checks the resulting output pixel
+
+class TinyGLTexEnvTestSuite : public CxxTest::TestSuite {
+    TinyGL::ContextHandle *_context = nullptr;
+public:
+    void setUp() {
+        _context = TinyGL::createContext(2, 2, Graphics::PixelFormat::createFormatARGB32(), 2, false, false);
+        TinyGL::setContext(_context);
+
+        tglEnable(TGL_TEXTURE_2D);
+        tglDisable(TGL_BLEND);
+        tglDisable(TGL_DEPTH_TEST);
+        tglMatrixMode(TGL_PROJECTION);
+        tglLoadIdentity();
+        tglMatrixMode(TGL_MODELVIEW);
+        tglLoadIdentity();
+		tglViewport(0, 0, 2, 2);
+    }
+
+    void tearDown() {
+        if (_context != nullptr) {
+            TinyGL::destroyContext(_context);
+            _context = nullptr;
+        }
+    }
+
+	// these two functions use RGBA order instead of ARGB to make it consistent with tglColor4ub which we also call
+
+    void drawPixel(byte texR, byte texG, byte texB, byte texA) {
+		const float S = 10.0f;
+        const byte texData[] = { texR, texG, texB, texA };
+        TGLuint texture;
+        tglGenTextures(1, &texture);
+        tglBindTexture(TGL_TEXTURE_2D, texture);
+        tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_WRAP_S, TGL_CLAMP);
+        tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_WRAP_T, TGL_CLAMP);
+        tglTexImage2D(TGL_TEXTURE_2D, 0, TGL_RGBA, 1, 1, 0, TGL_RGBA, TGL_UNSIGNED_BYTE, &texData);
+        tglBegin(TGL_TRIANGLES);
+        tglTexCoord2f(0.0f, 0.0f); tglVertex2f(-S, -S);
+        tglTexCoord2f(1.0f, 0.0f); tglVertex2f(+S, -S);
+        tglTexCoord2f(0.5f, 1.0f); tglVertex2f(0, +S);
+        tglEnd();
+    }
+
+    void checkOutput(byte expR, byte expG, byte expB, byte expA) {
+        byte actA, actR, actG, actB;
+        Graphics::Surface surface;
+        TinyGL::presentBuffer();
+        TinyGL::getSurfaceRef(surface);
+        surface.format.colorToARGB(surface.getPixel(0, 0), actA, actR, actG, actB);
+
+        TS_ASSERT_EQUALS(expA, actA);
+        TS_ASSERT_EQUALS(expR, actR);
+        TS_ASSERT_EQUALS(expG, actG);
+        TS_ASSERT_EQUALS(expB, actB);
+    }
+
+    void testModulate() {
+        // no tglTexEnvi setup because TGL_MODULATE should be the default
+        tglColor4ub(255, 255, 255, 127);
+        drawPixel(255, 127, 0, 255);
+        checkOutput(255, 127, 0, 127);
+    }
+
+    void testReplace() {
+        tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_REPLACE);
+        tglColor4ub(255, 255, 255, 127);
+        drawPixel(255, 127, 0, 255);
+        checkOutput(255, 127, 0, 255);
+    }
+
+    void testDecal() {
+        tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_DECAL);
+        tglColor4ub(100, 200, 255, 123);
+        drawPixel(200, 100, 0, 192);
+        checkOutput(176, 125, 63, 123);
+    }
+
+    void testAdd() {
+        tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_ADD);
+        tglColor4ub(50, 100, 150, 127);
+        drawPixel(25, 50, 150, 200);
+        checkOutput(75, 150, 255, 100);
+        // attention: TGL_ADD still modulates alpha
+    }
+
+    void setCombineMode(TGLuint combineRGB, TGLuint combineAlpha) {
+        tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_COMBINE);
+        tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_RGB, combineRGB);
+        tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_ALPHA, combineAlpha);
+    }
+
+    void setCombineArg(int arg, TGLuint rgbSource, TGLuint rgbOperand, TGLuint alphaSource, TGLuint alphaOperand) {
+        assert(arg >= 0 && arg <= 1);
+        tglTexEnvi(TGL_TEXTURE_ENV, arg ? TGL_SOURCE1_RGB : TGL_SOURCE0_RGB, rgbSource);
+        tglTexEnvi(TGL_TEXTURE_ENV, arg ? TGL_SOURCE1_ALPHA : TGL_SOURCE0_ALPHA, alphaSource);
+        tglTexEnvi(TGL_TEXTURE_ENV, arg ? TGL_OPERAND1_RGB : TGL_OPERAND0_RGB, rgbOperand);
+        tglTexEnvi(TGL_TEXTURE_ENV, arg ? TGL_OPERAND1_ALPHA : TGL_OPERAND0_ALPHA, alphaOperand);
+    }
+
+    void testCombineArgTexture() {
+        setCombineMode(TGL_REPLACE, TGL_REPLACE);
+        setCombineArg(0, TGL_TEXTURE, TGL_SRC_COLOR, TGL_TEXTURE, TGL_SRC_ALPHA);
+        drawPixel(13, 37, 42, 24);
+        checkOutput(13, 37, 42, 24);
+    }
+
+    void testCombineArgPrimaryColor() {
+        setCombineMode(TGL_REPLACE, TGL_REPLACE);
+        setCombineArg(0, TGL_PRIMARY_COLOR, TGL_SRC_COLOR, TGL_PRIMARY_COLOR, TGL_SRC_ALPHA);
+        tglColor4ub(12, 34, 56, 78);
+        drawPixel(13, 37, 42, 24);
+        checkOutput(12, 34, 56, 78);
+    }
+
+	void testCombineArgMixed() {
+		setCombineMode(TGL_REPLACE, TGL_REPLACE);
+		setCombineArg(0, TGL_PRIMARY_COLOR, TGL_SRC_ALPHA, TGL_TEXTURE, TGL_SRC_ALPHA);
+		tglColor4ub(12, 34, 56, 78);
+		drawPixel(13, 37, 42, 24);
+		checkOutput(78, 78, 78, 24);
+	}
+
+    void testCombineOpOneMinus() {
+        setCombineMode(TGL_REPLACE, TGL_REPLACE);
+        setCombineArg(0, TGL_TEXTURE, TGL_ONE_MINUS_SRC_COLOR, TGL_TEXTURE, TGL_ONE_MINUS_SRC_ALPHA);
+        drawPixel(13, 37, 42, 24);
+        checkOutput(242, 218, 213, 231);
+    }
+
+    void testCombineOpSrcAlpha() {
+        setCombineMode(TGL_REPLACE, TGL_REPLACE);
+        setCombineArg(0, TGL_TEXTURE, TGL_SRC_ALPHA, TGL_TEXTURE, TGL_ONE_MINUS_SRC_ALPHA);
+        drawPixel(13, 37, 42, 24);
+        checkOutput(24, 24, 24, 231);
+    }
+
+    void testCombineReplace() {
+        setCombineMode(TGL_REPLACE, TGL_REPLACE);
+        setCombineArg(0, TGL_TEXTURE, TGL_SRC_COLOR, TGL_TEXTURE, TGL_SRC_ALPHA);
+        setCombineArg(1, TGL_PRIMARY_COLOR, TGL_SRC_COLOR, TGL_PRIMARY_COLOR, TGL_SRC_ALPHA);
+        tglColor4ub(12, 34, 56, 78); // just to confuse the implementation
+        drawPixel(13, 37, 42, 24);
+        checkOutput(13, 37, 42, 24);
+    }
+
+    void testCombineModulate() {
+        setCombineMode(TGL_MODULATE, TGL_MODULATE);
+        setCombineArg(0, TGL_TEXTURE, TGL_SRC_COLOR, TGL_TEXTURE, TGL_SRC_ALPHA);
+        setCombineArg(1, TGL_PRIMARY_COLOR, TGL_SRC_COLOR, TGL_PRIMARY_COLOR, TGL_SRC_ALPHA);
+        tglColor4ub(255, 255, 255, 127);
+        drawPixel(255, 127, 0, 255);
+        checkOutput(255, 127, 0, 127);
+    }
+
+    void testCombineAdd() {
+        setCombineMode(TGL_ADD, TGL_ADD);
+        setCombineArg(0, TGL_TEXTURE, TGL_SRC_COLOR, TGL_TEXTURE, TGL_SRC_ALPHA);
+        setCombineArg(1, TGL_PRIMARY_COLOR, TGL_SRC_COLOR, TGL_PRIMARY_COLOR, TGL_SRC_ALPHA);
+        tglColor4ub(50, 100, 150, 100);
+        drawPixel(25, 50, 150, 200);
+        checkOutput(75, 150, 255, 255);
+        // attention: TGL_COMBINE TGL_ADD does *not* modulate alpha
+    }
+
+	void testSaveTexEnvState() {
+		tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_REPLACE);
+		tglColor4ub(0, 0, 0, 0);
+		drawPixel(255, 127, 0, 255);
+		tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_MODULATE); // before executing the drawcall
+		checkOutput(255, 127, 0, 255);
+	}
+};
+
+#endif


Commit: f6cf576e033807c748038f3288e82bdc130dd9ea
    https://github.com/scummvm/scummvm/commit/f6cf576e033807c748038f3288e82bdc130dd9ea
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-11T17:30:00+02:00

Commit Message:
TINYGL: Add fast-path for default texture environment

Changed paths:
    graphics/tinygl/texelbuffer.cpp
    graphics/tinygl/zbuffer.cpp
    graphics/tinygl/zbuffer.h
    graphics/tinygl/ztriangle.cpp


diff --git a/graphics/tinygl/texelbuffer.cpp b/graphics/tinygl/texelbuffer.cpp
index a4c747c6a68..0c9ce69ed6b 100644
--- a/graphics/tinygl/texelbuffer.cpp
+++ b/graphics/tinygl/texelbuffer.cpp
@@ -190,7 +190,7 @@ TexelBuffer *createNearestTexelBuffer(const byte *buf, const Graphics::PixelForm
 // usage increase should be negligible.
 class BilinearTexelBuffer : public TexelBuffer {
 public:
-	BilinearTexelBuffer(byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSiz, int internalformat);
+	BilinearTexelBuffer(byte *buf, const Graphics::PixelFormat &format, uint width, uint height, uint textureSize, int internalformat);
 	~BilinearTexelBuffer();
 
 protected:
diff --git a/graphics/tinygl/zbuffer.cpp b/graphics/tinygl/zbuffer.cpp
index 0fc653d284c..a6323669485 100644
--- a/graphics/tinygl/zbuffer.cpp
+++ b/graphics/tinygl/zbuffer.cpp
@@ -279,6 +279,25 @@ Graphics::Surface *copyFromFrameBuffer(const Graphics::PixelFormat &dstFormat) {
 	return c->fb->copyFromFrameBuffer(dstFormat);
 }
 
+static byte satAdd(byte a, byte b) {
+	// from: https://web.archive.org/web/20190213215419/https://locklessinc.com/articles/sat_arithmetic/
+	byte r = a + b;
+	return (byte)(r | -(r < a));
+};
+
+static byte sat16_to_8(uint32 x) {
+	x = (x + 128) >> 8; // rounding 16 to 8 
+	return (byte)(x | -!!(x >> 8)); // branchfree saturation
+};
+
+static byte fpMul(byte a, byte b) {
+	// from: https://community.khronos.org/t/precision-curiosity-1-255-or-1-256/40539/11
+	// correct would be (a*b)/255 but that is slow, instead we use (a*b) * 257/256 / 256
+	// this also implicitly saturates
+	uint32 r = a * b;
+	return (byte)((r + (r >> 8) + 127) >> 8);
+};
+
 void FrameBuffer::applyTextureEnvironment(
 	int internalformat,
 	uint previousA, uint previousR, uint previousG, uint previousB,
@@ -289,25 +308,6 @@ void FrameBuffer::applyTextureEnvironment(
 	// texARGB is both input and output
 	// GL_RGB/GL_RGBA might be identical as TexelBuffer returns As=1
 
-	const auto satAdd = [](byte a, byte b) -> byte {
-		// from: https://web.archive.org/web/20190213215419/https://locklessinc.com/articles/sat_arithmetic/
-		byte r = a + b;
-		return (byte)(r | -(r < a));
-	};
-
-	const auto sat16_to_8 = [](uint32 x) -> byte {
-		x = (x + 128) >> 8; // rounding 16 to 8 
-		return (byte)(x | -!!(x >> 8)); // branchfree saturation
-	};
-
-	const auto fpMul = [](byte a, byte b) -> byte {
-		// from: https://community.khronos.org/t/precision-curiosity-1-255-or-1-256/40539/11
-		// correct would be (a*b)/255 but that is slow, instead we use (a*b) * 257/256 / 256
-		// this also implicitly saturates
-		uint32 r = a * b;
-		return (byte)((r + (r >> 8) + 127) >> 8);
-	};
-
 	struct Arg {
 		byte a, r, g, b;
 	};
@@ -387,10 +387,7 @@ void FrameBuffer::applyTextureEnvironment(
 	{
 		// GL_RGB:  CpCs | Ap 
 		// GL_RGBA: CpCs | ApAs
-		texA = fpMul(sat16_to_8(previousA), texA);
-		texR = fpMul(sat16_to_8(previousR), texR);
-		texG = fpMul(sat16_to_8(previousG), texG);
-		texB = fpMul(sat16_to_8(previousB), texB);
+		applyModulation(previousA, previousR, previousG, previousB, texA, texR, texG, texB);
 		break;
 	}
 	case TGL_DECAL:
@@ -459,4 +456,13 @@ void FrameBuffer::applyTextureEnvironment(
 	}
 }
 
+void FrameBuffer::applyModulation(
+	uint previousA, uint previousR, uint previousG, uint previousB,
+	byte &texA, byte &texR, byte &texG, byte &texB) {
+	texA = fpMul(sat16_to_8(previousA), texA);
+	texR = fpMul(sat16_to_8(previousR), texR);
+	texG = fpMul(sat16_to_8(previousG), texG);
+	texB = fpMul(sat16_to_8(previousB), texB);
+}
+
 } // end of namespace TinyGL
diff --git a/graphics/tinygl/zbuffer.h b/graphics/tinygl/zbuffer.h
index 109a6bff8e2..37ef20711b0 100644
--- a/graphics/tinygl/zbuffer.h
+++ b/graphics/tinygl/zbuffer.h
@@ -364,7 +364,13 @@ private:
 	                       int &dzdx, int &drdx, int &dgdx, int &dbdx, uint dadx,
 	                       uint &fog, int fog_r, int fog_g, int fog_b, int &dfdx);
 
-	template <bool kDepthWrite, bool kLightsMode, bool kSmoothMode, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor, bool kEnableBlending, bool kStencilEnabled, bool kDepthTestEnabled>
+	enum class ColorMode {
+		NoInterpolation,
+		Default, // GL_TEXTURE_ENV_MODE == GL_MODULATE
+		CustomTexEnv
+	};
+
+	template <bool kDepthWrite, ColorMode kColorMode, bool kSmoothMode, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor, bool kEnableBlending, bool kStencilEnabled, bool kDepthTestEnabled>
 	void putPixelTexture(int fbOffset, const TexelBuffer *texture,
 	                     uint wrap_s, uint wrap_t, uint *pz, byte *ps, int _a,
 	                     int x, int y, uint &z, int &t, int &s,
@@ -707,44 +713,47 @@ private:
 	void selectOffscreenBuffer(Buffer *buffer);
 	void clearOffscreenBuffer(Buffer *buffer);
 
-	template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
+	template <ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
 	          bool kDepthWrite, bool kFogMode, bool kAlphaTestEnabled, bool kEnableScissor,
 	          bool kBlendingEnabled, bool kStencilEnabled, bool kStippleEnabled, bool kDepthTestEnabled>
 	void fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
 
-	template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
+	template <ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
 	          bool kDepthWrite, bool kFogMode, bool kAlphaTestEnabled, bool kEnableScissor,
 	          bool kBlendingEnabled, bool kStencilEnabled, bool kStippleEnabled>
 	void fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
 
-	template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
+	template <ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
 	          bool kDepthWrite, bool kFogMode, bool kAlphaTestEnabled, bool kEnableScissor,
 	          bool kBlendingEnabled, bool kStencilEnabled>
 	void fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
 
-	template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
+	template <ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
 	          bool kDepthWrite, bool kFogMode, bool enableAlphaTest, bool kEnableScissor, bool kBlendingEnabled>
 	void fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
 
-	template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
+	template <ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
 	          bool kDepthWrite, bool kFogMode, bool enableAlphaTest, bool kEnableScissor>
 	void fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
 
-	template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
+	template <ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
 	          bool kDepthWrite, bool kFogMode, bool enableAlphaTest>
 	void fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
 
-	template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
+	template <ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
 	          bool kDepthWrite, bool kFogMode>
 	void fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
 
-	template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
+	template <ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
 	          bool kDepthWrite>
 	void fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
 
-	template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode>
+	template <ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode>
 	void fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
 
+	template <bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite>
+	void fillTriangleTextureMapping(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
+
 public:
 
 	void fillTriangleTextureMappingPerspectiveSmooth(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2);
@@ -784,6 +793,11 @@ private:
 		uint previousA, uint previousR, uint previousG, uint previousB,
 		byte &texA, byte &texR, byte &texG, byte &texB);
 
+	// the same as GL_TEXTURE_ENV_MODE == GL_MODULATE as fast-path for the default mode
+	void applyModulation(
+		uint previousA, uint previousR, uint previousG, uint previousB,
+		byte &texA, byte &texR, byte &texG, byte &texB);
+
 	Buffer _offscreenBuffer;
 	byte *_pbuf;
 	int _pbufWidth;
diff --git a/graphics/tinygl/ztriangle.cpp b/graphics/tinygl/ztriangle.cpp
index e4bb9216e61..a8e0b5f47a5 100644
--- a/graphics/tinygl/ztriangle.cpp
+++ b/graphics/tinygl/ztriangle.cpp
@@ -92,7 +92,7 @@ end:
 	}
 }
 
-template <bool kDepthWrite, bool kLightsMode, bool kSmoothMode, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor, bool kEnableBlending, bool kStencilEnabled, bool kDepthTestEnabled>
+template <bool kDepthWrite, FrameBuffer::ColorMode kColorMode, bool kSmoothMode, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor, bool kEnableBlending, bool kStencilEnabled, bool kDepthTestEnabled>
 void FrameBuffer::putPixelTexture(int fbOffset, const TexelBuffer *texture,
                                   uint wrap_s, uint wrap_t, uint *pz, byte *ps, int _a,
                                   int x, int y, uint &z, int &t, int &s,
@@ -122,13 +122,22 @@ void FrameBuffer::putPixelTexture(int fbOffset, const TexelBuffer *texture,
 	if (depthTestResult) {
 		uint8 c_a, c_r, c_g, c_b;
 		texture->getARGBAt(wrap_s, wrap_t, s, t, c_a, c_r, c_g, c_b);
-		if (kLightsMode) {
-			// the name kLightsMode might be misleading, it is only false for
-			// depth-only triangles, in which case: sure, no texture env needed
+		switch (kColorMode) {
+		case ColorMode::NoInterpolation:
+			break;
+		case ColorMode::Default:
+			applyModulation(a, r, g, b, c_a, c_r, c_g, c_b);
+			break;
+		case ColorMode::CustomTexEnv:
 			applyTextureEnvironment(
 				texture->internalformat(),
 				a, r, g, b,
 				c_a, c_r, c_g, c_b);
+			break;
+		default:
+			// this would be a "if constexpr" and "static_assert" on C++17
+			assert(false && "Unimplemented color mode");
+			break;
 		}
 		writePixel<kEnableAlphaTest, kEnableBlending, kDepthWrite, kFogMode>(fbOffset + _a, c_a, c_r, c_g, c_b, z, fog, fog_r, fog_g, fog_b);
 	}
@@ -180,7 +189,7 @@ end:
 	z += dzdx;
 }
 
-template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
+template <FrameBuffer::ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode,
           bool kDepthWrite, bool kFogMode, bool kAlphaTestEnabled, bool kEnableScissor,
           bool kBlendingEnabled, bool kStencilEnabled, bool kStippleEnabled, bool kDepthTestEnabled>
 void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
@@ -248,7 +257,7 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 	fdx2 *= fz0;
 	fdy2 *= fz0;
 
-	if (kInterpRGB && kFogMode) {
+	if (kColorMode != ColorMode::NoInterpolation && kFogMode) {
 		fog_r = _fogColorR * 255;
 		fog_g = _fogColorG * 255;
 		fog_b = _fogColorB * 255;
@@ -265,7 +274,7 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 		dzdy = (int)(fdx1 * d2 - fdx2 * d1);
 	}
 
-	if (kInterpRGB && kSmoothMode) {
+	if (kColorMode != ColorMode::NoInterpolation && kSmoothMode) {
 		d1 = (float)(p1->r - p0->r);
 		d2 = (float)(p2->r - p0->r);
 		drdx = (int)(fdy2 * d1 - fdy1 * d2);
@@ -320,7 +329,7 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 	}
 
 	int polyOffset = 0;
-	if (kInterpZ && kInterpRGB && (_offsetStates & TGL_OFFSET_FILL)) {
+	if (kInterpZ && kColorMode != ColorMode::NoInterpolation && (_offsetStates & TGL_OFFSET_FILL)) {
 		int m = MAX(ABS(dzdx), ABS(dzdy));
 		polyOffset = -m * _offsetFactor + -_offsetUnits * (1 << 6);
 	}
@@ -333,14 +342,14 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 		ps1 = _sbuf + p0->y * _pbufWidth;
 	}
 
-	if (kInterpRGB && !kSmoothMode) {
+	if (kColorMode != ColorMode::NoInterpolation && !kSmoothMode) {
 		r1 = p2->r;
 		g1 = p2->g;
 		b1 = p2->b;
 		a1 = p2->a;
 	}
 
-	if (kInterpRGB && (kInterpST || kInterpSTZ)) {
+	if (kColorMode != ColorMode::NoInterpolation && (kInterpST || kInterpSTZ)) {
 		texture = _currentTexture;
 		fdzdx = (float)dzdx;
 		fndzdx = NB_INTERP * fdzdx;
@@ -391,7 +400,7 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 			dxdy_min = tmp >> 16;
 			dxdy_max = dxdy_min + 1;
 
-			if (kInterpRGB && kFogMode) {
+			if (kColorMode != ColorMode::NoInterpolation && kFogMode) {
 				f1 = l1->f;
 				dfdl_min = (dfdy + dfdx * dxdy_min);
 				dfdl_max = dfdl_min + dfdx;
@@ -403,7 +412,7 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 				dzdl_max = dzdl_min + dzdx;
 			}
 
-			if (kInterpRGB && kSmoothMode) {
+			if (kColorMode != ColorMode::NoInterpolation && kSmoothMode) {
 				r1 = l1->r;
 				drdl_min = (drdy + drdx * dxdy_min);
 				drdl_max = drdl_min + drdx;
@@ -447,7 +456,7 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 		// we draw all the scan line of the part
 		while (nb_lines > 0) {
 			int x = x1;
-			if (!kInterpRGB) {
+			if (kColorMode == ColorMode::NoInterpolation) {
 				int n;
 				uint *pz;
 				byte *ps = nullptr;
@@ -581,7 +590,7 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 						zinv = (float)(1.0 / fz);
 					}
 					for (int _a = 0; _a < NB_INTERP; _a++) {
-						putPixelTexture<kDepthWrite, kInterpRGB, kSmoothMode, kFogMode, kAlphaTestEnabled, kEnableScissor, kBlendingEnabled, kStencilEnabled, kDepthTestEnabled>
+						putPixelTexture<kDepthWrite, kColorMode, kSmoothMode, kFogMode, kAlphaTestEnabled, kEnableScissor, kBlendingEnabled, kStencilEnabled, kDepthTestEnabled>
 						               (pp, texture, _wrapS, _wrapT, pz, ps, _a, x, y, z, t, s, r, g, b, a, dzdx, dsdx, dtdx, drdx, dgdx, dbdx, dadx, fog, fog_r, fog_g, fog_b, dfdx);
 					}
 					pp += NB_INTERP;
@@ -608,7 +617,7 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 				}
 
 				while (n >= 0) {
-					putPixelTexture<kDepthWrite, kInterpRGB, kSmoothMode, kFogMode, kAlphaTestEnabled, kEnableScissor, kBlendingEnabled, kStencilEnabled, kDepthTestEnabled>
+					putPixelTexture<kDepthWrite, kColorMode, kSmoothMode, kFogMode, kAlphaTestEnabled, kEnableScissor, kBlendingEnabled, kStencilEnabled, kDepthTestEnabled>
 					               (pp, texture, _wrapS, _wrapT, pz, ps, 0, x, y, z, t, s, r, g, b, a, dzdx, dsdx, dtdx, drdx, dgdx, dbdx, dadx, fog, fog_r, fog_g, fog_b, dfdx);
 					pp += 1;
 					if (kInterpZ) {
@@ -627,13 +636,13 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 			if (error > 0) {
 				error -= 0x10000;
 				x1 += dxdy_max;
-				if (kInterpRGB && kFogMode) {
+				if (kColorMode != ColorMode::NoInterpolation && kFogMode) {
 					f1 += dfdl_max;
 				}
 				if (kInterpZ) {
 					z1 += dzdl_max;
 				}
-				if (kInterpRGB && kSmoothMode) {
+				if (kColorMode != ColorMode::NoInterpolation && kSmoothMode) {
 					r1 += drdl_max;
 					g1 += dgdl_max;
 					b1 += dbdl_max;
@@ -645,13 +654,13 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 				}
 			} else {
 				x1 += dxdy_min;
-				if (kInterpRGB && kFogMode) {
+				if (kColorMode != ColorMode::NoInterpolation && kFogMode) {
 					f1 += dfdl_min;
 				}
 				if (kInterpZ) {
 					z1 += dzdl_min;
 				}
-				if (kInterpRGB && kSmoothMode) {
+				if (kColorMode != ColorMode::NoInterpolation && kSmoothMode) {
 					r1 += drdl_min;
 					g1 += dgdl_min;
 					b1 += dbdl_min;
@@ -667,7 +676,7 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 			x2 += dx2dy2;
 
 			// screen coordinates
-			if (kInterpRGB) {
+			if (kColorMode != ColorMode::NoInterpolation) {
 				pp1 += _pbufWidth;
 			}
 			if (kInterpZ) {
@@ -683,128 +692,135 @@ void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint
 	}
 }
 
-template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor, bool kEnableBlending, bool kStencilEnabled, bool kStippleEnabled>
+template <FrameBuffer::ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor, bool kEnableBlending, bool kStencilEnabled, bool kStippleEnabled>
 void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	if (_depthTestEnabled) {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, kStencilEnabled, kStippleEnabled, true>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, kStencilEnabled, kStippleEnabled, true>(p0, p1, p2);
 	} else {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, kStencilEnabled, kStippleEnabled, false>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, kStencilEnabled, kStippleEnabled, false>(p0, p1, p2);
 	}
 }
 
-template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor, bool kEnableBlending, bool kStencilEnabled>
+template <FrameBuffer::ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor, bool kEnableBlending, bool kStencilEnabled>
 void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	if (_polygonStippleEnabled) {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, kStencilEnabled, true>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, kStencilEnabled, true>(p0, p1, p2);
 	} else {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, kStencilEnabled, false>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, kStencilEnabled, false>(p0, p1, p2);
 	}
 }
 
-template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor, bool kEnableBlending>
+template <FrameBuffer::ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor, bool kEnableBlending>
 void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	if (_sbuf && _stencilTestEnabled) {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, true>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, true>(p0, p1, p2);
 	} else {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, false>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, kEnableBlending, false>(p0, p1, p2);
 	}
 }
 
-template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor>
+template <FrameBuffer::ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode, bool kEnableAlphaTest, bool kEnableScissor>
 void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	if (_blendingEnabled) {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, true>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, true>(p0, p1, p2);
 	} else {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, false>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, kEnableScissor, false>(p0, p1, p2);
 	}
 }
 
-template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode, bool kEnableAlphaTest>
+template <FrameBuffer::ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode, bool kEnableAlphaTest>
 void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	if (_clippingEnabled) {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, true>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, true>(p0, p1, p2);
 	} else {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, false>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, kEnableAlphaTest, false>(p0, p1, p2);
 	}
 }
 
-template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode>
+template <FrameBuffer::ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite, bool kFogMode>
 void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	if (_alphaTestEnabled) {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, true>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, true>(p0, p1, p2);
 	} else {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, false>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, kFogMode, false>(p0, p1, p2);
 	}
 }
 
-template <bool kInterpRGB, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite>
+template <FrameBuffer::ColorMode kColorMode, bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite>
 void FrameBuffer::fillTriangle(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	if (_fogEnabled) {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, true>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, true>(p0, p1, p2);
 	}  else {
-		fillTriangle<kInterpRGB, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, false>(p0, p1, p2);
+		fillTriangle<kColorMode, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite, false>(p0, p1, p2);
 	}
 }
 
+template <bool kInterpZ, bool kInterpST, bool kInterpSTZ, bool kSmoothMode, bool kDepthWrite>
+void FrameBuffer::fillTriangleTextureMapping(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
+	// some color interpolation is implied by the texture mapping
+	if (_textureEnv->isDefault())
+		fillTriangle<ColorMode::Default, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite>(p0, p1, p2);
+	else
+		fillTriangle<ColorMode::CustomTexEnv, kInterpZ, kInterpST, kInterpSTZ, kSmoothMode, kDepthWrite>(p0, p1, p2);
+}
+
 void FrameBuffer::fillTriangleDepthOnly(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	const bool interpZ = true;
-	const bool interpRGB = false;
+	const ColorMode colorMode = ColorMode::NoInterpolation;
 	const bool interpST = false;
 	const bool interpSTZ = false;
 	const bool smoothMode = false;
 	if (_depthWrite && _depthTestEnabled)
-		fillTriangle<interpRGB, interpZ, interpST, interpSTZ, smoothMode, true>(p0, p1, p2);
+		fillTriangle<colorMode, interpZ, interpST, interpSTZ, smoothMode, true>(p0, p1, p2);
 	else
-		fillTriangle<interpRGB, interpZ, interpST, interpSTZ, smoothMode, false>(p0, p1, p2);
+		fillTriangle<colorMode, interpZ, interpST, interpSTZ, smoothMode, false>(p0, p1, p2);
 }
 
 void FrameBuffer::fillTriangleFlat(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	const bool interpZ = true;
-	const bool interpRGB = true;
+	const ColorMode colorMode = ColorMode::Default;
 	const bool interpST = false;
 	const bool interpSTZ = false;
 	const bool smoothMode = false;
 	if (_depthWrite && _depthTestEnabled)
-		fillTriangle<interpRGB, interpZ, interpST, interpSTZ, smoothMode, true>(p0, p1, p2);
+		fillTriangle<colorMode, interpZ, interpST, interpSTZ, smoothMode, true>(p0, p1, p2);
 	else
-		fillTriangle<interpRGB, interpZ, interpST, interpSTZ, smoothMode, false>(p0, p1, p2);
+		fillTriangle<colorMode, interpZ, interpST, interpSTZ, smoothMode, false>(p0, p1, p2);
 }
 
 // Smooth filled triangle.
 void FrameBuffer::fillTriangleSmooth(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	const bool interpZ = true;
-	const bool interpRGB = true;
+	const ColorMode colorMode = ColorMode::Default;
 	const bool interpST = false;
 	const bool interpSTZ = false;
 	const bool smoothMode = true;
 	if (_depthWrite && _depthTestEnabled)
-		fillTriangle<interpRGB, interpZ, interpST, interpSTZ, smoothMode, true>(p0, p1, p2);
+		fillTriangle<colorMode, interpZ, interpST, interpSTZ, smoothMode, true>(p0, p1, p2);
 	else
-		fillTriangle<interpRGB, interpZ, interpST, interpSTZ, smoothMode, false>(p0, p1, p2);
+		fillTriangle<colorMode, interpZ, interpST, interpSTZ, smoothMode, false>(p0, p1, p2);
 }
 
 void FrameBuffer::fillTriangleTextureMappingPerspectiveSmooth(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	const bool interpZ = true;
-	const bool interpRGB = true;
 	const bool interpST = true;
 	const bool interpSTZ = true;
 	const bool smoothMode = true;
 	if (_depthWrite && _depthTestEnabled)
-		fillTriangle<interpRGB, interpZ, interpST, interpSTZ, smoothMode, true>(p0, p1, p2);
+		fillTriangleTextureMapping<interpZ, interpST, interpSTZ, smoothMode, true>(p0, p1, p2);
 	else
-		fillTriangle<interpRGB, interpZ, interpST, interpSTZ, smoothMode, false>(p0, p1, p2);
+		fillTriangleTextureMapping<interpZ, interpST, interpSTZ, smoothMode, false>(p0, p1, p2);
 }
 
 void FrameBuffer::fillTriangleTextureMappingPerspectiveFlat(ZBufferPoint *p0, ZBufferPoint *p1, ZBufferPoint *p2) {
 	const bool interpZ = true;
-	const bool interpRGB = true;
 	const bool interpST = false;
 	const bool interpSTZ = true;
 	const bool smoothMode = false;
 	if (_depthWrite && _depthTestEnabled)
-		fillTriangle<interpRGB, interpZ, interpST, interpSTZ, smoothMode, true>(p0, p1, p2);
+		fillTriangleTextureMapping<interpZ, interpST, interpSTZ, smoothMode, true>(p0, p1, p2);
 	else
-		fillTriangle<interpRGB, interpZ, interpST, interpSTZ, smoothMode, false>(p0, p1, p2);
+		fillTriangleTextureMapping<interpZ, interpST, interpSTZ, smoothMode, false>(p0, p1, p2);
 }
 
 } // end of namespace TinyGL


Commit: 3567d572119b2b9d3f1401866995bdb6c16087ac
    https://github.com/scummvm/scummvm/commit/3567d572119b2b9d3f1401866995bdb6c16087ac
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-11T17:30:00+02:00

Commit Message:
TESTBED: Add texenv comparisons between TinyGL and OpenGL

Changed paths:
  A dists/engine-data/testbed-audiocd-files/image/pm5544-32bpp-grayalpha.png
  A engines/testbed/tinygl.cpp
  A engines/testbed/tinygl.h
    dists/engine-data/testbed-audiocd-files/image/image-gen.sh
    engines/testbed/configure.engine
    engines/testbed/module.mk
    engines/testbed/testbed.cpp


diff --git a/dists/engine-data/testbed-audiocd-files/image/image-gen.sh b/dists/engine-data/testbed-audiocd-files/image/image-gen.sh
index 9b72f92f07e..58c8f72007a 100755
--- a/dists/engine-data/testbed-audiocd-files/image/image-gen.sh
+++ b/dists/engine-data/testbed-audiocd-files/image/image-gen.sh
@@ -25,4 +25,5 @@ magick $1 -depth 8 -type Grayscale $base-8bpp-grey.png
 magick $1 -depth 8 -type Grayscale $base-8bpp-grey.tga
 magick $1 -depth 8 -type Grayscale -compress RLE $base-8bpp-grey-rle.tga
 magick $1 -depth 1 -type Grayscale $base-1bpp.xbm
+magick $1 -channel-fx 'gray=>alpha' $base-32bpp-grayalpha.png
 
diff --git a/dists/engine-data/testbed-audiocd-files/image/pm5544-32bpp-grayalpha.png b/dists/engine-data/testbed-audiocd-files/image/pm5544-32bpp-grayalpha.png
new file mode 100644
index 00000000000..3edb90b9eec
Binary files /dev/null and b/dists/engine-data/testbed-audiocd-files/image/pm5544-32bpp-grayalpha.png differ
diff --git a/engines/testbed/configure.engine b/engines/testbed/configure.engine
index 4d6e7bfc63c..1fce680a777 100644
--- a/engines/testbed/configure.engine
+++ b/engines/testbed/configure.engine
@@ -1,3 +1,3 @@
 # This file is included from the main "configure" script
 # add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps] [components]
-add_engine testbed "TestBed: the Testing framework" no "" "" "" "imgui midi universaltracker indeo3 indeo45 vpx mpc hnm mpeg2 qdm2 svq1"
+add_engine testbed "TestBed: the Testing framework" no "" "" "" "imgui midi universaltracker indeo3 indeo45 vpx mpc hnm mpeg2 qdm2 svq1 tinygl"
diff --git a/engines/testbed/module.mk b/engines/testbed/module.mk
index 76bbdcff655..2c002897d5b 100644
--- a/engines/testbed/module.mk
+++ b/engines/testbed/module.mk
@@ -37,6 +37,13 @@ MODULE_OBJS += \
 	imgui.o
 endif
 
+ifdef USE_TINYGL
+ifdef USE_OPENGL_GAME # Currently only direct comparisons are implemented
+MODULE_OBJS += \
+    tinygl.o
+endif
+endif
+
 MODULE_DIRS += \
 	engines/testbed
 
diff --git a/engines/testbed/testbed.cpp b/engines/testbed/testbed.cpp
index a149a120bd9..3d04993888f 100644
--- a/engines/testbed/testbed.cpp
+++ b/engines/testbed/testbed.cpp
@@ -57,6 +57,9 @@
 #ifdef USE_IMGUI
 #include "testbed/imgui.h"
 #endif
+#if defined USE_TINYGL && defined USE_OPENGL_GAME
+#include "testbed/tinygl.h"
+#endif
 
 namespace Testbed {
 
@@ -192,6 +195,11 @@ void TestbedEngine::pushTestsuites(Common::Array<Testsuite *> &testsuiteList) {
 	// Video decoder
 	ts = new VideoDecoderTestSuite();
 	testsuiteList.push_back(ts);
+#if defined USE_TINYGL && defined USE_OPENGL_GAME
+	// TinyGL
+	ts = new TinyGLTestSuite();
+	testsuiteList.push_back(ts);
+#endif
 }
 
 TestbedEngine::~TestbedEngine() {
diff --git a/engines/testbed/tinygl.cpp b/engines/testbed/tinygl.cpp
new file mode 100644
index 00000000000..0d24fab8994
--- /dev/null
+++ b/engines/testbed/tinygl.cpp
@@ -0,0 +1,385 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/file.h"
+#include "engines/util.h"
+#include "image/png.h"
+#include "graphics/managed_surface.h"
+#include "graphics/opengl/system_headers.h"
+#include "graphics/opengl/debug.h"
+#include "graphics/tinygl/tinygl.h"
+
+#include "testbed/tinygl.h"
+
+namespace Testbed {
+
+namespace TinyGLTests {
+	struct TextureEnvironmentArg {
+		GLuint _sourceRGB, _operandRGB;
+		GLuint _sourceAlpha, _operandAlpha;
+
+		TextureEnvironmentArg(
+			GLuint sourceRGB = GL_TEXTURE,
+			GLuint operandRGB = GL_SRC_COLOR,
+			GLuint sourceAlpha = GL_TEXTURE,
+			GLuint operandAlpha = GL_SRC_ALPHA)
+			: _sourceRGB(sourceRGB)
+			, _operandRGB(operandRGB)
+			, _sourceAlpha(sourceAlpha)
+			, _operandAlpha(operandAlpha) { }
+	};
+
+	struct TextureEnvironment {
+		GLuint _mode, _combineRGB, _combineAlpha;
+		TextureEnvironmentArg _arg0, _arg1;
+
+		TextureEnvironment(
+			GLuint mode = GL_REPLACE,
+			GLuint combineRGB = GL_REPLACE,
+			GLuint combineAlpha = GL_REPLACE)
+			: _mode(mode)
+			, _combineRGB(combineRGB)
+			, _combineAlpha(combineAlpha) { }
+
+		template<typename TexEnvFunc>
+		void apply(TexEnvFunc func) const {
+			func(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, _mode);
+			func(GL_TEXTURE_ENV, GL_COMBINE_RGB, _combineRGB);
+			func(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, _combineAlpha);
+
+			func(GL_TEXTURE_ENV, GL_SOURCE0_RGB, _arg0._sourceRGB);
+			func(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, _arg0._sourceAlpha);
+			func(GL_TEXTURE_ENV, GL_SOURCE1_RGB, _arg1._sourceRGB);
+			func(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, _arg1._sourceAlpha);
+
+			func(GL_TEXTURE_ENV, GL_OPERAND0_RGB, _arg0._operandRGB);
+			func(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, _arg0._operandAlpha);
+			func(GL_TEXTURE_ENV, GL_OPERAND1_RGB, _arg1._operandRGB);
+			func(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, _arg1._operandAlpha);
+		}
+	};
+
+	TestExitStatus runTexEnvTest(
+		const char *testName,
+		const TextureEnvironment &env,
+		byte colorR, byte colorG, byte colorB, byte colorA);
+	TestExitStatus testTexEnvReplace();
+	TestExitStatus testTexEnvModulate();
+	TestExitStatus testTexEnvDecal();
+	TestExitStatus testTexEnvAdd();
+	TestExitStatus testTexEnvCombineOpNormal();
+	TestExitStatus testTexEnvCombineOpInverse();
+	TestExitStatus testTexEnvCombineOpAlphaToColor();
+	TestExitStatus testTexEnvCombineMixedArgs();
+	TestExitStatus testTexEnvCombineReplace();
+	TestExitStatus testTexEnvCombineModulate();
+	TestExitStatus testTexEnvCombineAdd();
+}
+
+TinyGLTestSuite::TinyGLTestSuite() {
+	addTest("Replace", &TinyGLTests::testTexEnvReplace);
+	addTest("Modulate", &TinyGLTests::testTexEnvModulate);
+	addTest("Decal", &TinyGLTests::testTexEnvDecal);
+	addTest("Add", &TinyGLTests::testTexEnvAdd);
+	addTest("CombineOpNormal", &TinyGLTests::testTexEnvCombineOpNormal);
+	addTest("CombineOpInverse", &TinyGLTests::testTexEnvCombineOpInverse);
+	addTest("CombineOpAlphaToColor", &TinyGLTests::testTexEnvCombineOpAlphaToColor);
+	addTest("CombineMixedArgs", &TinyGLTests::testTexEnvCombineMixedArgs);
+	addTest("CombineReplace", &TinyGLTests::testTexEnvCombineReplace);
+	addTest("CombineModulate", &TinyGLTests::testTexEnvCombineModulate);
+	addTest("CombineAdd", TinyGLTests::testTexEnvCombineAdd);
+}
+
+TestExitStatus TinyGLTests::testTexEnvModulate() {
+	TextureEnvironment env(GL_MODULATE);
+	return runTexEnvTest("Modulate", env, 50, 111, 222, 255);
+}
+
+TestExitStatus TinyGLTests::testTexEnvReplace() {
+	TextureEnvironment env(GL_REPLACE);
+	return runTexEnvTest("Replace", env, 255, 0, 255, 255);
+}
+
+TestExitStatus TinyGLTests::testTexEnvDecal() {
+	TextureEnvironment env(GL_DECAL);
+	return runTexEnvTest("Decal", env, 50, 111, 222, 255);
+}
+
+TestExitStatus TinyGLTests::testTexEnvAdd() {
+	TextureEnvironment env(GL_ADD);
+	return runTexEnvTest("Add", env, 50, 111, 222, 255);
+}
+
+TestExitStatus TinyGLTests::testTexEnvCombineOpNormal() {
+	TextureEnvironment env(GL_COMBINE, GL_REPLACE, GL_REPLACE);
+	env._arg0 = { GL_TEXTURE, GL_SRC_COLOR, GL_TEXTURE, GL_SRC_ALPHA };
+	return runTexEnvTest("CombineOpNormal", env, 50, 111, 222, 255);
+}
+
+TestExitStatus TinyGLTests::testTexEnvCombineOpInverse() {
+	TextureEnvironment env(GL_COMBINE, GL_REPLACE, GL_REPLACE);
+	env._arg0 = { GL_TEXTURE, GL_ONE_MINUS_SRC_COLOR, GL_TEXTURE, GL_ONE_MINUS_SRC_ALPHA };
+	return runTexEnvTest("CombineOpInverse", env, 50, 111, 222, 255);
+}
+
+TestExitStatus TinyGLTests::testTexEnvCombineOpAlphaToColor() {
+	TextureEnvironment env(GL_COMBINE, GL_REPLACE, GL_REPLACE);
+	env._arg0 = { GL_TEXTURE, GL_SRC_ALPHA, GL_TEXTURE, GL_ONE_MINUS_SRC_ALPHA };
+	return runTexEnvTest("CombineOpAlphaToColor", env, 50, 111, 222, 255);
+}
+
+TestExitStatus TinyGLTests::testTexEnvCombineMixedArgs() {
+	TextureEnvironment env(GL_COMBINE, GL_REPLACE, GL_REPLACE);
+	env._arg0 = { GL_PRIMARY_COLOR, GL_SRC_ALPHA, GL_TEXTURE, GL_SRC_ALPHA };
+	return runTexEnvTest("CombineMixedArgs", env, 255, 0, 255, 50);
+}
+
+TestExitStatus TinyGLTests::testTexEnvCombineReplace() {
+	TextureEnvironment env(GL_COMBINE, GL_REPLACE, GL_REPLACE);
+	env._arg0 = { GL_TEXTURE, GL_SRC_COLOR, GL_PRIMARY_COLOR, GL_SRC_ALPHA };
+	return runTexEnvTest("CombineReplace", env, 50, 11, 222, 127);
+}
+
+TestExitStatus TinyGLTests::testTexEnvCombineModulate() {
+	TextureEnvironment env(GL_COMBINE, GL_MODULATE, GL_MODULATE);
+	env._arg0 = { GL_TEXTURE, GL_SRC_COLOR, GL_TEXTURE, GL_SRC_ALPHA };
+	env._arg1 = { GL_PRIMARY_COLOR, GL_SRC_COLOR, GL_PRIMARY_COLOR, GL_SRC_ALPHA };
+	return runTexEnvTest("CombineModulate", env, 50, 11, 222, 127);
+}
+
+TestExitStatus TinyGLTests::testTexEnvCombineAdd() {
+	TextureEnvironment env(GL_COMBINE, GL_ADD, GL_ADD);
+	env._arg0 = { GL_TEXTURE, GL_SRC_COLOR, GL_TEXTURE, GL_SRC_ALPHA };
+	env._arg1 = { GL_PRIMARY_COLOR, GL_SRC_COLOR, GL_PRIMARY_COLOR, GL_SRC_ALPHA };
+	return runTexEnvTest("CombineAdd", env, 50, 11, 222, 127);
+}
+
+struct TinyGLContextDeleter {
+	inline void operator()(TinyGL::ContextHandle *handle) {
+		TinyGL::destroyContext(handle);
+	}
+};
+
+// using cdecl calling convention so we can use a function pointer
+static void classicGLTexEnvi(GLenum target, GLenum param, GLint value) {
+	glTexEnvi(target, param, value);
+}
+
+static void copyAlphaIntoColorChannels(Graphics::ManagedSurface &surface) {
+	byte a, r, g, b;
+	for (int y = 0; y < surface.h; y++) {
+		uint32 *pixel = (uint32*)surface.getBasePtr(0, y);
+		for (int x = 0; x < surface.w; x++, pixel++) {
+			surface.format.colorToARGB(*pixel, a, r, g, b);
+			r = g = b = a;
+			a = 255;
+			*pixel = surface.format.ARGBToColor(a, r, g, b);
+		}
+	}
+}
+
+static constexpr const int kScreenWidth = 800; // enough space for a 2x2 grid and some padding
+static constexpr const int kScreenHeight = 600;
+static constexpr const int kTextureSize = 256;
+
+static void renderClassicQuad(GLuint texture, float x, float y, bool flipV = false) {
+	const float tLow = flipV ? 1.0f : 0.0f;
+	const float tHigh = flipV ? 0.0f : 1.0f;
+	glEnable(GL_TEXTURE_2D);
+	glBindTexture(GL_TEXTURE_2D, texture);
+	glBegin(GL_TRIANGLE_STRIP);
+	glTexCoord2f(0.0f, tLow); glVertex2f(x, y);
+	glTexCoord2f(1.0f, tLow); glVertex2f(x + kTextureSize, y);
+	glTexCoord2f(0.0f, tHigh); glVertex2f(x, y + kTextureSize);
+	glTexCoord2f(1.0f, tHigh); glVertex2f(x + kTextureSize, y + kTextureSize);
+	glEnd();
+}
+
+static void renderClassicGridQuad(GLuint texture, int row, int column) {
+	renderClassicQuad(
+		texture,
+		kScreenWidth / 4 * (row * 2 + 1) - kTextureSize / 2,
+		kScreenHeight / 4 * (column * 2 + 1) - kTextureSize / 2,
+		true); // by rendering and reading back we have flipped the image
+}
+
+TestExitStatus TinyGLTests::runTexEnvTest(
+	const char *testName,
+	const TextureEnvironment &env,
+	byte colorR, byte colorG, byte colorB, byte colorA) {
+
+	int oldW = g_system->getWidth();
+	int oldH = g_system->getHeight();
+	auto oldFormat = g_system->getScreenFormat();
+	int oldGraphicsMode = g_system->getGraphicsMode();
+
+	// load test image (crop and scale to square power-of-two)
+	Graphics::ManagedSurface testImage;
+	{
+		constexpr const char *kImagePath = "image/pm5544-32bpp-grayalpha.png";
+		Image::PNGDecoder pngDecoder;
+		Common::File file;
+		if (!file.open(kImagePath) ||
+			!pngDecoder.loadStream(file)) {
+			Testsuite::logDetailedPrintf("Error! Could not load test image: %s\n", kImagePath);
+			return kTestFailed;
+		}
+		const auto *pngSurface = pngDecoder.getSurface();
+		if (pngSurface->w < 240 || pngSurface->h < 240) {
+			Testsuite::logDetailedPrintf("Error! Test image has unexpected size: %dx%d\n", pngSurface->w, pngSurface->h);
+			return kTestFailed;
+		}
+		int16 pngSize = MIN(pngSurface->w, pngSurface->h);
+		auto subRect = Common::Rect::center(pngSurface->w / 2, pngSurface->h / 2, pngSize, pngSize);
+
+		Graphics::ManagedSurface converted;
+		converted.convertFrom(*pngSurface, Graphics::PixelFormat::createFormatRGBA32());
+		testImage.create(kTextureSize, kTextureSize, Graphics::PixelFormat::createFormatRGBA32());
+		Graphics::scaleBlitBilinear(
+			(byte *)testImage.getBasePtr(0, 0),
+			(const byte *)converted.getBasePtr(subRect.left, subRect.top),
+			testImage.pitch,
+			converted.pitch,
+			testImage.w, testImage.h,
+			converted.w, converted.h,
+			converted.format);
+	}
+	Graphics::ManagedSurface readBack(testImage.w, testImage.h, testImage.format);
+
+	// Initialize classic opengl
+	initGraphics3d(kScreenWidth, kScreenHeight);
+	glMatrixMode(GL_MODELVIEW);
+	glLoadIdentity();
+	glMatrixMode(GL_PROJECTION);
+	glLoadIdentity();
+	glOrtho(0, kScreenWidth, 0, kScreenHeight, -1, 1);
+	glViewport(0, 0, kScreenWidth, kScreenHeight);
+	glDisable(GL_BLEND);
+	glDisable(GL_DEPTH_TEST);
+	glDisable(GL_CULL_FACE);
+	glEnable(GL_TEXTURE_2D);
+	glClearColor(0, 0, 0, 0);
+	GLuint classicTextures[5]; // test texture + 2*classic result textures + 2*TinyGL result textures
+	glGenTextures(5, classicTextures);
+	GLuint classicTestTexture = classicTextures[0];
+	glBindTexture(GL_TEXTURE_2D, classicTestTexture);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTextureSize, kTextureSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, testImage.getPixels());
+
+	// Initialize TinyGL
+	Common::ScopedPtr<TinyGL::ContextHandle, TinyGLContextDeleter> tglContext(
+		TinyGL::createContext(kTextureSize, kTextureSize, testImage.format, kTextureSize, false, false));
+	TinyGL::setContext(tglContext.get());
+	Graphics::Surface tinyglSurface;
+	TinyGL::getSurfaceRef(tinyglSurface);
+	tglMatrixMode(TGL_MODELVIEW);
+	tglLoadIdentity();
+	tglMatrixMode(TGL_PROJECTION);
+	tglLoadIdentity();
+	tglOrtho(0, kScreenWidth, 0, kScreenHeight, -1, 1);
+	tglViewport(0, 0, kScreenWidth, kScreenHeight);
+	tglDisable(TGL_BLEND);
+	tglDisable(TGL_DEPTH_TEST);
+	tglDisable(TGL_CULL_FACE);
+	tglEnable(TGL_TEXTURE_2D);
+	tglClearColor(0, 0, 0, 0);
+	TGLuint tinyglTestTexture;
+	tglGenTextures(1, &tinyglTestTexture);
+	tglBindTexture(TGL_TEXTURE_2D, classicTestTexture);
+	tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_MIN_FILTER, TGL_NEAREST);
+	tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_MAG_FILTER, TGL_NEAREST);
+	tglTexImage2D(TGL_TEXTURE_2D, 0, TGL_RGBA, kTextureSize, kTextureSize, 0, TGL_RGBA, TGL_UNSIGNED_BYTE, testImage.getPixels());
+
+	// Render classic OpenGL
+	env.apply(classicGLTexEnvi);
+	glClear(GL_COLOR_BUFFER_BIT);
+	glColor4ub(colorR, colorG, colorB, colorA);
+	renderClassicQuad(classicTestTexture, 0, 0);
+	glFlush();
+	g_system->presentBuffer();
+	readBack.clear();
+	glReadPixels(0, 0, kTextureSize, kTextureSize, GL_RGBA, GL_UNSIGNED_BYTE, readBack.getPixels());
+	GLuint classicReadBackColorTex = classicTextures[1];
+	glBindTexture(GL_TEXTURE_2D, classicReadBackColorTex);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTextureSize, kTextureSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, readBack.getPixels());
+	copyAlphaIntoColorChannels(readBack);
+	GLuint classicReadBackAlphaTex = classicTextures[2];
+	glBindTexture(GL_TEXTURE_2D, classicReadBackAlphaTex);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTextureSize, kTextureSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, readBack.getPixels());
+
+	// Render TinyGL
+	env.apply(tglTexEnvi);
+	tglClear(TGL_COLOR_BUFFER_BIT);
+	tglColor4ub(colorR, colorG, colorB, colorA);
+	tglBegin(TGL_TRIANGLE_STRIP);
+	tglTexCoord2f(0.0f, 1.0f); tglVertex2f(0.0f, 0.0f);
+	tglTexCoord2f(1.0f, 1.0f); tglVertex2f(kTextureSize, 0.0f);
+	tglTexCoord2f(0.0f, 0.0f); tglVertex2f(0.0f, kTextureSize);
+	tglTexCoord2f(1.0f, 0.0f); tglVertex2f(kTextureSize, kTextureSize);
+	tglEnd();
+	TinyGL::presentBuffer();
+	readBack.copyFrom(tinyglSurface);
+	GLuint tinyglReadBackColorTex = classicTextures[3];
+	glBindTexture(GL_TEXTURE_2D, tinyglReadBackColorTex);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTextureSize, kTextureSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, readBack.getPixels());
+	copyAlphaIntoColorChannels(readBack);
+	GLuint tinyglReadBackAlphaTex = classicTextures[4];
+	glBindTexture(GL_TEXTURE_2D, tinyglReadBackAlphaTex);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTextureSize, kTextureSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, readBack.getPixels());
+
+	// Render comparison
+	glClear(GL_COLOR_BUFFER_BIT);
+	glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
+	glColor4f(1, 1, 1, 1);
+	renderClassicGridQuad(classicReadBackColorTex, 0, 0);
+	renderClassicGridQuad(classicReadBackAlphaTex, 1, 0);
+	renderClassicGridQuad(tinyglReadBackColorTex, 0, 1);
+	renderClassicGridQuad(tinyglReadBackAlphaTex, 1, 1);
+	glFlush();
+	g_system->updateScreen();
+	glDeleteTextures(5, classicTextures);
+
+	g_system->delayMillis(1000);
+	TestExitStatus status = kTestPassed;
+	Common::String info = Common::String::format("Does the top row of images look like the bottom row?\n(Testing %s)", testName);
+	if (Testsuite::handleInteractiveInput(info, "Yes", "No", kOptionRight)) {
+		Testsuite::logDetailedPrintf("Error! TinyGL and OpenGL have different texure environment behaviors for %s\n", testName);
+		status = kTestFailed;
+	}
+
+	// Return to previous state
+	g_system->beginGFXTransaction();
+	g_system->setGraphicsMode(oldGraphicsMode);
+	g_system->initSize(oldW, oldH, &oldFormat);
+	g_system->endGFXTransaction();
+	return status;
+}
+
+}
diff --git a/engines/testbed/tinygl.h b/engines/testbed/tinygl.h
new file mode 100644
index 00000000000..7b81b8dec57
--- /dev/null
+++ b/engines/testbed/tinygl.h
@@ -0,0 +1,53 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef TESTBED_TINYGL_H
+#define TESTBED_TINYGL_H
+
+#include "testbed/testsuite.h"
+
+namespace Testbed {
+
+class TinyGLTestSuite : public Testsuite {
+public:
+	/**
+	 * The constructor for the XXXTestSuite
+	 * For every test to be executed one must:
+	 * 1) Create a function that would invoke the test
+	 * 2) Add that test to list by executing addTest()
+	 *
+	 * @see addTest()
+	 */
+	TinyGLTestSuite();
+	~TinyGLTestSuite() override = default;
+	const char *getName() const override {
+		return "TinyGL";
+	}
+
+	const char *getDescription() const override {
+		return "Comparing TinyGL output with classic OpenGL";
+	}
+
+};
+
+} // End of namespace Testbed
+
+#endif // TESTBED_TEMPLATE_H


Commit: ee25b1f3f389aac8b61bcf95c75bfa1de0f1ef63
    https://github.com/scummvm/scummvm/commit/ee25b1f3f389aac8b61bcf95c75bfa1de0f1ef63
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-11T17:30:00+02:00

Commit Message:
TINYGL: Add TGL_CONSTANT and TGL_BLEND

Changed paths:
    graphics/tinygl/api.cpp
    graphics/tinygl/gl.h
    graphics/tinygl/texture.cpp
    graphics/tinygl/zbuffer.cpp
    graphics/tinygl/zbuffer.h
    graphics/tinygl/zdirtyrect.cpp
    graphics/tinygl/zdirtyrect.h
    test/tgraphics/tinygl_texenv.h


diff --git a/graphics/tinygl/api.cpp b/graphics/tinygl/api.cpp
index 9e962e3904d..d82bdce6d16 100644
--- a/graphics/tinygl/api.cpp
+++ b/graphics/tinygl/api.cpp
@@ -742,6 +742,24 @@ void tglTexEnvi(TGLenum target, TGLenum pname, TGLint param) {
 	c->gl_add_op(p);
 }
 
+void tglTexEnvfv(TGLenum target, TGLenum pname, const TGLfloat *params) {
+	TinyGL::GLContext *c = TinyGL::gl_get_context();
+	TinyGL::GLParam p[8];
+	
+	p[0].op = TinyGL::OP_TexEnv;
+	p[1].i = target;
+	p[2].i = pname;
+	p[3].i = 0;
+
+	int n = 0;
+	if (target == TGL_TEXTURE_ENV && pname == TGL_TEXTURE_ENV_COLOR)
+		n = 4;
+	for (int i = 0; i < n; i++)
+		p[4 + i].f = params[i];
+
+	c->gl_add_op(p);
+}
+
 void tglTexParameteri(TGLenum target, TGLenum pname, TGLint param) {
 	TinyGL::GLContext *c = TinyGL::gl_get_context();
 	TinyGL::GLParam p[8];
diff --git a/graphics/tinygl/gl.h b/graphics/tinygl/gl.h
index 9e33c972c18..37626998dc7 100644
--- a/graphics/tinygl/gl.h
+++ b/graphics/tinygl/gl.h
@@ -674,6 +674,7 @@ enum {
 	TGL_OPERAND1_RGB				= 0x8591,
 	TGL_OPERAND0_ALPHA				= 0x8598,
 	TGL_OPERAND1_ALPHA				= 0x8599,
+	TGL_CONSTANT					= 0x8576,
 	TGL_PRIMARY_COLOR				= 0x8577,
 	TGL_PREVIOUS					= 0x8578,
 
diff --git a/graphics/tinygl/texture.cpp b/graphics/tinygl/texture.cpp
index 3be2cba08ca..6172c66d36c 100644
--- a/graphics/tinygl/texture.cpp
+++ b/graphics/tinygl/texture.cpp
@@ -190,7 +190,7 @@ void GLContext::glopTexEnv(GLParam *p) {
 
 	if (target != TGL_TEXTURE_ENV) {
 error:
-		error("tglTexParameter: unsupported option");
+		error("tglTexEnv: unsupported option");
 	}
 
 	switch (pname) {
@@ -198,7 +198,7 @@ error:
 		if (param == TGL_REPLACE ||
 			param == TGL_MODULATE ||
 			param == TGL_DECAL ||
-			//param == TGL_BLEND || // no tex env constants yet
+			param == TGL_BLEND ||
 			param == TGL_ADD ||
 			param == TGL_COMBINE)
 			_texEnv.envMode = param;
@@ -226,7 +226,8 @@ error:
 	{
 		GLTextureEnvArgument *op = pname == TGL_SOURCE0_RGB ? &_texEnv.arg0 : &_texEnv.arg1;
 		if (param == TGL_TEXTURE ||
-			param == TGL_PRIMARY_COLOR)
+			param == TGL_PRIMARY_COLOR ||
+			param == TGL_CONSTANT)
 			op->sourceRGB = param;
 		else
 			goto error;
@@ -237,7 +238,8 @@ error:
 	{
 		GLTextureEnvArgument *op = pname == TGL_SOURCE0_ALPHA ? &_texEnv.arg0 : &_texEnv.arg1;
 		if (param == TGL_TEXTURE ||
-			param == TGL_PRIMARY_COLOR)
+			param == TGL_PRIMARY_COLOR ||
+			param == TGL_CONSTANT)
 			op->sourceAlpha = param;
 		else
 			goto error;
@@ -266,6 +268,14 @@ error:
 			goto error;
 		break;
 	}
+	case TGL_TEXTURE_ENV_COLOR:
+	{
+		_texEnv.constR = (byte)clampf(p[4].f * 255.0f, 0, 255.0f);
+		_texEnv.constG = (byte)clampf(p[5].f * 255.0f, 0, 255.0f);
+		_texEnv.constB = (byte)clampf(p[6].f * 255.0f, 0, 255.0f);
+		_texEnv.constA = (byte)clampf(p[7].f * 255.0f, 0, 255.0f);
+		break;
+	}
 	default:
 		goto error;
 	}
diff --git a/graphics/tinygl/zbuffer.cpp b/graphics/tinygl/zbuffer.cpp
index a6323669485..6c8a080a7a8 100644
--- a/graphics/tinygl/zbuffer.cpp
+++ b/graphics/tinygl/zbuffer.cpp
@@ -328,6 +328,12 @@ void FrameBuffer::applyTextureEnvironment(
 			opColor.g = sat16_to_8(previousG);
 			opColor.b = sat16_to_8(previousB);
 			break;
+		case TGL_CONSTANT:
+			opColor.a = _textureEnv->constA;
+			opColor.r = _textureEnv->constR;
+			opColor.g = _textureEnv->constG;
+			opColor.b = _textureEnv->constB;
+			break;
 		default:
 			assert(false && "Invalid texture environment arg color source");
 			break;
@@ -339,6 +345,9 @@ void FrameBuffer::applyTextureEnvironment(
 		case TGL_PRIMARY_COLOR:
 			op.a = sat16_to_8(previousA);
 			break;
+		case TGL_CONSTANT:
+			op.a = _textureEnv->constA;
+			break;
 		default:
 			assert(false && "Invalid texture environment arg alpha source");
 			break;
@@ -402,14 +411,24 @@ void FrameBuffer::applyTextureEnvironment(
 	}
 	case TGL_ADD:
 	{
-		// GL_RGB: Cp + Cs | Ap
-		// GL_RGB: Cp + Cs | ApAs
+		// GL_RGB:  Cp + Cs | Ap
+		// GL_RGBA: Cp + Cs | ApAs
 		texA = fpMul(sat16_to_8(previousA), texA);
 		texR = satAdd(sat16_to_8(previousR), texR);
 		texG = satAdd(sat16_to_8(previousG), texG);
 		texB = satAdd(sat16_to_8(previousB), texB);
 		break;
 	}
+	case TGL_BLEND:
+	{
+		// GL_RGB:  Cp(1-Cs) + CcCs | Ap
+		// GL_RGBA: Cp(1-Cs) + CcCs | ApAs
+		texA = fpMul(sat16_to_8(previousA), texA);
+		texR = satAdd(fpMul(sat16_to_8(previousR), 255 - texR), fpMul(_textureEnv->constR, texR));
+		texG = satAdd(fpMul(sat16_to_8(previousG), 255 - texG), fpMul(_textureEnv->constG, texG));
+		texB = satAdd(fpMul(sat16_to_8(previousB), 255 - texB), fpMul(_textureEnv->constB, texB));
+		break;
+	}
 	case TGL_COMBINE:
 	{
 		Arg arg0 = getCombineArg(_textureEnv->arg0);
diff --git a/graphics/tinygl/zbuffer.h b/graphics/tinygl/zbuffer.h
index 37ef20711b0..6a73a28b337 100644
--- a/graphics/tinygl/zbuffer.h
+++ b/graphics/tinygl/zbuffer.h
@@ -76,7 +76,7 @@ static const int DRAW_DEPTH_ONLY = 0;
 static const int DRAW_FLAT = 1;
 static const int DRAW_SMOOTH = 2;
 
-struct GLTextureEnv; // defined in zgl.h
+struct GLTextureEnv; // defined in zdirtyrect.h
 
 struct Buffer {
 	byte *pbuf;
diff --git a/graphics/tinygl/zdirtyrect.cpp b/graphics/tinygl/zdirtyrect.cpp
index 7a14a426a0e..b34947905fa 100644
--- a/graphics/tinygl/zdirtyrect.cpp
+++ b/graphics/tinygl/zdirtyrect.cpp
@@ -36,7 +36,8 @@ GLTextureEnvArgument::GLTextureEnvArgument()
 GLTextureEnv::GLTextureEnv()
 	: envMode(TGL_MODULATE)
 	, combineRGB(TGL_REPLACE)
-	, combineAlpha(TGL_REPLACE) {}
+	, combineAlpha(TGL_REPLACE)
+	, constA(255), constR(255), constG(255), constB(255) {}
 
 bool GLTextureEnv::isDefault() const {
 	return envMode == TGL_MODULATE;
diff --git a/graphics/tinygl/zdirtyrect.h b/graphics/tinygl/zdirtyrect.h
index 569049dd53a..e0c614eac32 100644
--- a/graphics/tinygl/zdirtyrect.h
+++ b/graphics/tinygl/zdirtyrect.h
@@ -53,6 +53,7 @@ struct GLTextureEnv {
 	bool isDefault() const;
 
 	uint envMode, combineRGB, combineAlpha;
+	byte constA, constR, constG, constB;
 	GLTextureEnvArgument arg0, arg1;
 };
 
diff --git a/test/tgraphics/tinygl_texenv.h b/test/tgraphics/tinygl_texenv.h
index 9d08bdd5c7f..e31b161b616 100644
--- a/test/tgraphics/tinygl_texenv.h
+++ b/test/tgraphics/tinygl_texenv.h
@@ -31,7 +31,12 @@ public:
         }
     }
 
-	// these two functions use RGBA order instead of ARGB to make it consistent with tglColor4ub which we also call
+	// these three functions use RGBA order instead of ARGB to make it consistent with tglColor4ub which we also call
+
+    void setConstant(byte r, byte g, byte b, byte a) {
+        const float values[] = { r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f };
+        tglTexEnvfv(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_COLOR, values);
+    }
 
     void drawPixel(byte texR, byte texG, byte texB, byte texA) {
 		const float S = 10.0f;
@@ -91,6 +96,15 @@ public:
         // attention: TGL_ADD still modulates alpha
     }
 
+    void testBlend() {
+        tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_BLEND);
+        tglColor4ub(210, 210, 200, 127);
+        setConstant(123, 123, 100, 42);
+        drawPixel(0, 255, 128, 200);
+        checkOutput(211, 123, 150, 100);
+		// the 211 is an unfortunate rounding problem
+    }
+
     void setCombineMode(TGLuint combineRGB, TGLuint combineAlpha) {
         tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_COMBINE);
         tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_RGB, combineRGB);
@@ -120,6 +134,14 @@ public:
         checkOutput(12, 34, 56, 78);
     }
 
+    void testCombineArgConstant() {
+        setCombineMode(TGL_REPLACE, TGL_REPLACE);
+        setCombineArg(0, TGL_CONSTANT, TGL_SRC_COLOR, TGL_CONSTANT, TGL_SRC_ALPHA);
+        setConstant(12, 34, 56, 78);
+        drawPixel(13, 37, 42, 24);
+        checkOutput(12, 34, 56, 78);
+    }
+
 	void testCombineArgMixed() {
 		setCombineMode(TGL_REPLACE, TGL_REPLACE);
 		setCombineArg(0, TGL_PRIMARY_COLOR, TGL_SRC_ALPHA, TGL_TEXTURE, TGL_SRC_ALPHA);


Commit: 27f2b1b6d6520e0130dc75592a315984bada2df9
    https://github.com/scummvm/scummvm/commit/27f2b1b6d6520e0130dc75592a315984bada2df9
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-11T17:30:00+02:00

Commit Message:
TESTBED: Add test for TGL_BLEND

Changed paths:
    engines/testbed/tinygl.cpp


diff --git a/engines/testbed/tinygl.cpp b/engines/testbed/tinygl.cpp
index 0d24fab8994..4b824e6e0db 100644
--- a/engines/testbed/tinygl.cpp
+++ b/engines/testbed/tinygl.cpp
@@ -50,6 +50,7 @@ namespace TinyGLTests {
 	struct TextureEnvironment {
 		GLuint _mode, _combineRGB, _combineAlpha;
 		TextureEnvironmentArg _arg0, _arg1;
+		byte _constantR = 255, _constantG = 255, _constantB = 255, _constantA = 255;
 
 		TextureEnvironment(
 			GLuint mode = GL_REPLACE,
@@ -59,21 +60,29 @@ namespace TinyGLTests {
 			, _combineRGB(combineRGB)
 			, _combineAlpha(combineAlpha) { }
 
-		template<typename TexEnvFunc>
-		void apply(TexEnvFunc func) const {
-			func(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, _mode);
-			func(GL_TEXTURE_ENV, GL_COMBINE_RGB, _combineRGB);
-			func(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, _combineAlpha);
-
-			func(GL_TEXTURE_ENV, GL_SOURCE0_RGB, _arg0._sourceRGB);
-			func(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, _arg0._sourceAlpha);
-			func(GL_TEXTURE_ENV, GL_SOURCE1_RGB, _arg1._sourceRGB);
-			func(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, _arg1._sourceAlpha);
-
-			func(GL_TEXTURE_ENV, GL_OPERAND0_RGB, _arg0._operandRGB);
-			func(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, _arg0._operandAlpha);
-			func(GL_TEXTURE_ENV, GL_OPERAND1_RGB, _arg1._operandRGB);
-			func(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, _arg1._operandAlpha);
+		template<typename TexEnvFunci, typename TexEnvFuncfv>
+		void apply(TexEnvFunci funci, TexEnvFuncfv funcfv) const {
+			funci(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, _mode);
+			funci(GL_TEXTURE_ENV, GL_COMBINE_RGB, _combineRGB);
+			funci(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, _combineAlpha);
+
+			funci(GL_TEXTURE_ENV, GL_SOURCE0_RGB, _arg0._sourceRGB);
+			funci(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, _arg0._sourceAlpha);
+			funci(GL_TEXTURE_ENV, GL_SOURCE1_RGB, _arg1._sourceRGB);
+			funci(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, _arg1._sourceAlpha);
+
+			funci(GL_TEXTURE_ENV, GL_OPERAND0_RGB, _arg0._operandRGB);
+			funci(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, _arg0._operandAlpha);
+			funci(GL_TEXTURE_ENV, GL_OPERAND1_RGB, _arg1._operandRGB);
+			funci(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, _arg1._operandAlpha);
+
+			const float values[] = {
+				_constantR / 255.0f,
+				_constantG / 255.0f,
+				_constantB / 255.0f,
+				_constantA / 255.0f
+			};
+			funcfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, values);
 		}
 	};
 
@@ -85,6 +94,7 @@ namespace TinyGLTests {
 	TestExitStatus testTexEnvModulate();
 	TestExitStatus testTexEnvDecal();
 	TestExitStatus testTexEnvAdd();
+	TestExitStatus testTexEnvBlend();
 	TestExitStatus testTexEnvCombineOpNormal();
 	TestExitStatus testTexEnvCombineOpInverse();
 	TestExitStatus testTexEnvCombineOpAlphaToColor();
@@ -99,6 +109,7 @@ TinyGLTestSuite::TinyGLTestSuite() {
 	addTest("Modulate", &TinyGLTests::testTexEnvModulate);
 	addTest("Decal", &TinyGLTests::testTexEnvDecal);
 	addTest("Add", &TinyGLTests::testTexEnvAdd);
+	addTest("Blend", &TinyGLTests::testTexEnvBlend);
 	addTest("CombineOpNormal", &TinyGLTests::testTexEnvCombineOpNormal);
 	addTest("CombineOpInverse", &TinyGLTests::testTexEnvCombineOpInverse);
 	addTest("CombineOpAlphaToColor", &TinyGLTests::testTexEnvCombineOpAlphaToColor);
@@ -128,6 +139,15 @@ TestExitStatus TinyGLTests::testTexEnvAdd() {
 	return runTexEnvTest("Add", env, 50, 111, 222, 255);
 }
 
+TestExitStatus TinyGLTests::testTexEnvBlend() {
+	TextureEnvironment env(GL_BLEND);
+	env._constantR = 250;
+	env._constantG = 150;
+	env._constantB = 100;
+	env._constantA = 50;
+	return runTexEnvTest("Blend", env, 50, 111, 222, 255);
+}
+
 TestExitStatus TinyGLTests::testTexEnvCombineOpNormal() {
 	TextureEnvironment env(GL_COMBINE, GL_REPLACE, GL_REPLACE);
 	env._arg0 = { GL_TEXTURE, GL_SRC_COLOR, GL_TEXTURE, GL_SRC_ALPHA };
@@ -183,6 +203,10 @@ static void classicGLTexEnvi(GLenum target, GLenum param, GLint value) {
 	glTexEnvi(target, param, value);
 }
 
+static void classicGLTexEnvfv(GLenum target, GLenum param, const GLfloat *values) {
+	glTexEnvfv(target, param, values);
+}
+
 static void copyAlphaIntoColorChannels(Graphics::ManagedSurface &surface) {
 	byte a, r, g, b;
 	for (int y = 0; y < surface.h; y++) {
@@ -310,7 +334,7 @@ TestExitStatus TinyGLTests::runTexEnvTest(
 	tglTexImage2D(TGL_TEXTURE_2D, 0, TGL_RGBA, kTextureSize, kTextureSize, 0, TGL_RGBA, TGL_UNSIGNED_BYTE, testImage.getPixels());
 
 	// Render classic OpenGL
-	env.apply(classicGLTexEnvi);
+	env.apply(classicGLTexEnvi, classicGLTexEnvfv);
 	glClear(GL_COLOR_BUFFER_BIT);
 	glColor4ub(colorR, colorG, colorB, colorA);
 	renderClassicQuad(classicTestTexture, 0, 0);
@@ -331,7 +355,7 @@ TestExitStatus TinyGLTests::runTexEnvTest(
 	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTextureSize, kTextureSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, readBack.getPixels());
 
 	// Render TinyGL
-	env.apply(tglTexEnvi);
+	env.apply(tglTexEnvi, tglTexEnvfv);
 	tglClear(TGL_COLOR_BUFFER_BIT);
 	tglColor4ub(colorR, colorG, colorB, colorA);
 	tglBegin(TGL_TRIANGLE_STRIP);


Commit: 27638abef614fd301e3e339947793a89594042f8
    https://github.com/scummvm/scummvm/commit/27638abef614fd301e3e339947793a89594042f8
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-11T17:30:00+02:00

Commit Message:
TETRAEDGE: Enable texture environments on TinyGL

Changed paths:
    engines/tetraedge/te/te_mesh_tinygl.cpp
    engines/tetraedge/te/te_renderer_tinygl.cpp


diff --git a/engines/tetraedge/te/te_mesh_tinygl.cpp b/engines/tetraedge/te/te_mesh_tinygl.cpp
index f100363eebe..a2b93c3583e 100644
--- a/engines/tetraedge/te/te_mesh_tinygl.cpp
+++ b/engines/tetraedge/te/te_mesh_tinygl.cpp
@@ -28,7 +28,7 @@
 
 namespace Tetraedge {
 
-TeMeshTinyGL::TeMeshTinyGL() : _glMeshMode(TGL_POINTS), _gltexEnvMode(TGL_DECAL) {
+TeMeshTinyGL::TeMeshTinyGL() : _glMeshMode(TGL_POINTS), _gltexEnvMode(TGL_MODULATE) {
 }
 
 void TeMeshTinyGL::draw() {
@@ -99,8 +99,7 @@ void TeMeshTinyGL::draw() {
 	if (!_colors.empty())
 		tglColorPointer(4, TGL_UNSIGNED_BYTE, sizeof(TeColor), _colors.data());
 
-	// TODO: not supported in TGL
-	//tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, _gltexEnvMode);
+	tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, _gltexEnvMode);
 	if (renderer->scissorEnabled()) {
 		tglEnable(TGL_SCISSOR_TEST);
 		// TODO: Scissor not supported by TGL
@@ -141,8 +140,7 @@ void TeMeshTinyGL::draw() {
 	if (!renderer->scissorEnabled())
 		tglDisable(TGL_SCISSOR_TEST);
 
-	// TODO: GL_MODULATE not supported in TGL
-	//tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_DECAL);
+	tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_MODULATE);
 	tglDisableClientState(TGL_VERTEX_ARRAY);
 	tglDisableClientState(TGL_NORMAL_ARRAY);
 	tglDisableClientState(TGL_COLOR_ARRAY);
diff --git a/engines/tetraedge/te/te_renderer_tinygl.cpp b/engines/tetraedge/te/te_renderer_tinygl.cpp
index eaa1dce076c..9d0077d21a6 100644
--- a/engines/tetraedge/te/te_renderer_tinygl.cpp
+++ b/engines/tetraedge/te/te_renderer_tinygl.cpp
@@ -208,8 +208,7 @@ void TeRendererTinyGL::renderTransparentMeshes() {
 					  meshProperties._scissorWidth,
 					  meshProperties._scissorHeight);*/
 		}
-		// TODO: not supported in TGL
-		//tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_DECAL/*meshProperties._glTexEnvMode*/);
+		tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, meshProperties._glTexEnvMode);
 		tglDrawElements(TGL_TRIANGLES, meshProperties._vertexCount, TGL_UNSIGNED_SHORT,
 				   _transparentMeshVertexNums.data() + vertsDrawn);
 
@@ -219,8 +218,7 @@ void TeRendererTinyGL::renderTransparentMeshes() {
 			tglEnableClientState(TGL_TEXTURE_COORD_ARRAY);
 			tglEnableClientState(TGL_COLOR_ARRAY);
 		}
-		// TODO: not supported in TGL
-		//tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_DECAL);
+		tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_MODULATE);
 		if (meshProperties._scissorEnabled) {
 			tglDisable(TGL_SCISSOR_TEST);
 		}
@@ -306,7 +304,7 @@ void TeRendererTinyGL::shadowMode(enum ShadowMode mode) {
 
 void TeRendererTinyGL::applyMaterial(const TeMaterial &m) {
 	//debug("TeMaterial::apply (%s)", dump().c_str());
-	//static const float constColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
+	static const float constColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
 	if (_shadowMode == TeRenderer::ShadowModeNone) {
 		if (m._enableLights)
 			TeLightTinyGL::enableAll();
@@ -321,8 +319,6 @@ void TeRendererTinyGL::applyMaterial(const TeMaterial &m) {
 
 		tglDisable(TGL_ALPHA_TEST);
 		if (m._mode == TeMaterial::MaterialMode0) {
-			/* TODO: Find TGL equivalents for this stuff*/
-			/*
 			tglTexEnvfv(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_COLOR, constColor);
 			tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_COMBINE);
 			tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_RGB, TGL_MODULATE);
@@ -331,10 +327,9 @@ void TeRendererTinyGL::applyMaterial(const TeMaterial &m) {
 			tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_ALPHA, TGL_REPLACE);
 			tglTexEnvi(TGL_TEXTURE_ENV, TGL_SOURCE0_ALPHA, TGL_CONSTANT);
 			tglTexEnvi(TGL_TEXTURE_ENV, TGL_OPERAND0_ALPHA, TGL_SRC_ALPHA);
-			*/
+			
 		} else {
-			// TODO: GL_MODULATE supported in TGL
-			//tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_DECAL);
+			tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_MODULATE);
 			if (m._mode != TeMaterial::MaterialMode1) {
 				tglEnable(TGL_ALPHA_TEST);
 				tglAlphaFunc(TGL_GREATER, 0.5);
@@ -365,8 +360,7 @@ void TeRendererTinyGL::applyMaterial(const TeMaterial &m) {
 		static const float fullColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
 		TeLightTinyGL::disableAll();
 		tglDisable(TGL_ALPHA_TEST);
-		// TODO: GL_MODULATE not supported in TGL
-		//tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_DECAL);
+		tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_MODULATE);
 		tglMaterialfv(TGL_FRONT_AND_BACK, TGL_AMBIENT, fullColor);
 		tglMaterialfv(TGL_FRONT_AND_BACK, TGL_DIFFUSE, fullColor);
 		tglMaterialfv(TGL_FRONT_AND_BACK, TGL_SPECULAR, fullColor);
@@ -379,8 +373,7 @@ void TeRendererTinyGL::applyMaterial(const TeMaterial &m) {
 		tglDisable(TGL_TEXTURE_GEN_R);
 		tglDisable(TGL_TEXTURE_GEN_Q);
 	} else {
-		// TODO: GL_MODULATE not supported in TGL
-		//tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_DECAL);
+		tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_MODULATE);
 		tglEnable(TGL_TEXTURE_GEN_S);
 		tglEnable(TGL_TEXTURE_GEN_T);
 		tglEnable(TGL_TEXTURE_GEN_R);
@@ -389,8 +382,7 @@ void TeRendererTinyGL::applyMaterial(const TeMaterial &m) {
 		TeLightTinyGL::disableAll();
 		tglDisable(TGL_ALPHA_TEST);
 		enableTexture();
-		// TODO: GL_MODULATE not supported in TGL
-		//tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_DECAL);
+		tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_MODULATE);
 
 		const float diffuse[4] = { m._diffuseColor.r() / 255.0f, m._diffuseColor.g() / 255.0f,
 			m._diffuseColor.b() / 255.0f, m._diffuseColor.a() / 255.0f };




More information about the Scummvm-git-logs mailing list