[Scummvm-git-logs] scummvm master -> c66e0f4fa3da891ee9615565077bb6890276a8be

neuromancer noreply at scummvm.org
Thu Apr 30 16:56:09 UTC 2026


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

Summary:
8368df463a COLONY: initial code for shaders renderer
a6056fe555 COLONY: added basic shaders support for 2d primitives
e0b482a360 COLONY: added basic shaders support for unoptimized 3d primitives
47442da883 COLONY: optimize shader 3d primitives code
22276d5926 COLONY: support for stipples in shader renderer
4f4b855c9d COLONY: add shader files into distribution files
ca3f853772 COLONY: deleted scroll interface
c66e0f4fa3 COLONY: enable SupportsArbitraryResolutions


Commit: 8368df463a0d13bd5ac6d750d799beaa3ea1e3fe
    https://github.com/scummvm/scummvm/commit/8368df463a0d13bd5ac6d750d799beaa3ea1e3fe
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-30T18:55:53+02:00

Commit Message:
COLONY: initial code for shaders renderer

Changed paths:
  A engines/colony/renderer_opengl_shaders.cpp
  A engines/colony/renderer_opengl_shaders.h
    engines/colony/gfx.cpp
    engines/colony/module.mk


diff --git a/engines/colony/gfx.cpp b/engines/colony/gfx.cpp
index 52627e3da9c..829d456b2ca 100644
--- a/engines/colony/gfx.cpp
+++ b/engines/colony/gfx.cpp
@@ -27,19 +27,81 @@
 
 #include "common/config-manager.h"
 #include "common/system.h"
+#include "common/textconsole.h"
 #include "engines/util.h"
+#include "graphics/renderer.h"
 
 #include "colony/renderer.h"
+#include "colony/renderer_opengl_shaders.h"
 
 namespace Colony {
 
-// Forward declaration for the OpenGL renderer factory
+// Forward declaration for the fixed-function OpenGL renderer factory.
 Renderer *createOpenGLRenderer(OSystem *system, int width, int height);
 
+// Pick the renderer type. Honors --renderer=<code> on the command line /
+// ConfMan key, restricted to what was compiled in.
+//
+// Phase 1 policy: default users get fixed-function OpenGL. The shader path
+// is opt-in (--renderer=opengl_shaders) until later phases bring its
+// primitive coverage up to parity. Note that the generic preference order
+// in graphics/renderer.cpp:122 picks shaders for the Default case, so we
+// must override it here.
+static Graphics::RendererType pickRendererType() {
+	const Common::String configured = ConfMan.get("renderer");
+	const Graphics::RendererType desired = Graphics::Renderer::parseTypeCode(configured);
+
+	if (desired == Graphics::kRendererTypeDefault) {
+#ifdef USE_OPENGL_GAME
+		return Graphics::kRendererTypeOpenGL;
+#elif defined(USE_OPENGL_SHADERS)
+		return Graphics::kRendererTypeOpenGLShaders;
+#else
+		return Graphics::kRendererTypeDefault;
+#endif
+	}
+
+	const uint32 supported =
+#ifdef USE_OPENGL_GAME
+		Graphics::kRendererTypeOpenGL |
+#endif
+#ifdef USE_OPENGL_SHADERS
+		Graphics::kRendererTypeOpenGLShaders |
+#endif
+		0;
+	const Graphics::RendererType matching =
+		Graphics::Renderer::getBestMatchingAvailableType(desired, supported);
+
+	if (matching != desired)
+		warning("Colony: requested renderer '%s' is unavailable, falling back",
+			configured.c_str());
+	return matching;
+}
+
 Renderer *createRenderer(OSystem *system, int width, int height) {
-	// Always use OpenGL (following Freescape's pattern for accelerated renderers)
+	const Graphics::RendererType type = pickRendererType();
 	initGraphics3d(width, height);
-	return createOpenGLRenderer(system, width, height);
+
+#if defined(USE_OPENGL_SHADERS)
+	if (type == Graphics::kRendererTypeOpenGLShaders) {
+		Renderer *r = createOpenGLShaderRenderer(system, width, height);
+		if (r)
+			return r;
+		warning("Colony: shader renderer factory returned null, falling back to fixed-function");
+	}
+#endif
+
+#if defined(USE_OPENGL_GAME)
+	if (type == Graphics::kRendererTypeOpenGL || type == Graphics::kRendererTypeDefault)
+		return createOpenGLRenderer(system, width, height);
+#endif
+
+	// Last resort: try fixed-function unconditionally so the engine still
+	// runs in builds where neither flag was caught above.
+	Renderer *r = createOpenGLRenderer(system, width, height);
+	if (!r)
+		error("Colony: no renderer available (built without OpenGL support?)");
+	return r;
 }
 
 } // End of namespace Colony
diff --git a/engines/colony/module.mk b/engines/colony/module.mk
index f2d1a05b782..ef172a65071 100644
--- a/engines/colony/module.mk
+++ b/engines/colony/module.mk
@@ -15,6 +15,7 @@ MODULE_OBJS := \
 	render_features.o \
 	render_objects.o \
 	renderer_opengl.o \
+	renderer_opengl_shaders.o \
 	savegame.o \
 	sound.o \
 	think.o \
diff --git a/engines/colony/renderer_opengl_shaders.cpp b/engines/colony/renderer_opengl_shaders.cpp
new file mode 100644
index 00000000000..d7911f51997
--- /dev/null
+++ b/engines/colony/renderer_opengl_shaders.cpp
@@ -0,0 +1,117 @@
+/* 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/>.
+ *
+ * Based on the original sources
+ *   https://github.com/Croquetx/thecolony
+ * Copyright (C) 1988, David A. Smith
+ *
+ * Distributed under Apache Version 2.0 License
+ *
+ */
+
+#include "common/scummsys.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "colony/renderer.h"
+#include "colony/renderer_opengl_shaders.h"
+
+#ifdef USE_OPENGL_SHADERS
+
+#include "graphics/opengl/system_headers.h"
+
+namespace Colony {
+
+// Phase 1: skeleton only. Every Renderer override is a stub. Callers can
+// build with USE_OPENGL_SHADERS, instantiate the renderer (e.g. via
+// `--renderer=opengl_shaders`), but the screen will be empty until later
+// phases fill in primitives, 3D draws, and the surface blit path.
+//
+// The fixed-function path (`renderer_opengl.cpp`) remains the default and
+// is unaffected by this file.
+class OpenGLShaderRenderer : public Renderer {
+public:
+	OpenGLShaderRenderer(OSystem *system, int width, int height)
+		: _system(system), _width(width), _height(height) {
+		(void)_width;  // populated for Phase 2+ primitive implementations
+		(void)_height;
+		warning("Colony: OpenGL shader renderer is a Phase 1 skeleton — "
+			"primitives are stubbed; use --renderer=opengl for the working renderer");
+	}
+
+	~OpenGLShaderRenderer() override {}
+
+	// 2D primitives — stubbed.
+	void clear(uint32 color) override {}
+	void drawLine(int x1, int y1, int x2, int y2, uint32 color) override {}
+	void drawRect(const Common::Rect &rect, uint32 color) override {}
+	void fillRect(const Common::Rect &rect, uint32 color) override {}
+	void drawString(const Graphics::Font *font, const Common::String &str, int x, int y,
+			uint32 color, Graphics::TextAlign align) override {}
+	void scroll(int dx, int dy, uint32 background) override {}
+	void drawEllipse(int x, int y, int rx, int ry, uint32 color) override {}
+	void fillEllipse(int x, int y, int rx, int ry, uint32 color) override {}
+	void fillDitherRect(const Common::Rect &rect, uint32 c1, uint32 c2) override {}
+	void setPixel(int x, int y, uint32 color) override {}
+	void drawQuad(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, uint32 color) override {}
+	void drawPolygon(const int *x, const int *y, int count, uint32 color) override {}
+
+	void setPalette(const byte *palette, uint start, uint count) override {}
+
+	// 3D scene rendering — stubbed.
+	void begin3D(int camX, int camY, int camZ, int angle, int angleY, const Common::Rect &viewport) override {}
+	void draw3DWall(int x1, int y1, int x2, int y2, uint32 color) override {}
+	void draw3DQuad(float x1, float y1, float z1, float x2, float y2, float z2,
+			float x3, float y3, float z3, float x4, float y4, float z4, uint32 color) override {}
+	void draw3DPolygon(const float *x, const float *y, const float *z, int count, uint32 color) override {}
+	void draw3DLine(float x1, float y1, float z1, float x2, float y2, float z2, uint32 color) override {}
+	void end3D() override {}
+
+	// Buffer / state management.
+	void copyToScreen() override { _system->updateScreen(); }
+	void setWireframe(bool enable, int64_t fillColor) override {}
+	void setXorMode(bool enable) override {}
+	void setStippleData(const byte *data) override {}
+	void setMacColors(uint32 fg, uint32 bg) override {}
+	void setDepthState(bool testEnabled, bool writeEnabled) override {}
+	void setDepthRange(float nearVal, float farVal) override {}
+	void computeScreenViewport() override {}
+
+	void drawSurface(const Graphics::Surface *surf, int x, int y) override {}
+	Graphics::Surface *getScreenshot() override { return nullptr; }
+
+private:
+	OSystem *_system = nullptr;
+	int _width = 0;
+	int _height = 0;
+};
+
+Renderer *createOpenGLShaderRenderer(OSystem *system, int width, int height) {
+	return new OpenGLShaderRenderer(system, width, height);
+}
+
+} // End of namespace Colony
+
+#else
+
+namespace Colony {
+Renderer *createOpenGLShaderRenderer(OSystem *system, int width, int height) { return nullptr; }
+}
+
+#endif
diff --git a/engines/colony/renderer_opengl_shaders.h b/engines/colony/renderer_opengl_shaders.h
new file mode 100644
index 00000000000..5e45c4a1b27
--- /dev/null
+++ b/engines/colony/renderer_opengl_shaders.h
@@ -0,0 +1,44 @@
+/* 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/>.
+ *
+ * Based on the original sources
+ *   https://github.com/Croquetx/thecolony
+ * Copyright (C) 1988, David A. Smith
+ *
+ * Distributed under Apache Version 2.0 License
+ *
+ */
+
+#ifndef COLONY_RENDERER_OPENGL_SHADERS_H
+#define COLONY_RENDERER_OPENGL_SHADERS_H
+
+#include "common/scummsys.h"
+
+namespace Colony {
+
+class Renderer;
+
+// Factory for the programmable-pipeline OpenGL renderer. Returns nullptr
+// when USE_OPENGL_SHADERS is not defined at build time, so callers can
+// fall back to the fixed-function renderer.
+Renderer *createOpenGLShaderRenderer(OSystem *system, int width, int height);
+
+} // End of namespace Colony
+
+#endif


Commit: a6056fe555cc91aa342c02b1289a3a7a00b0bd6a
    https://github.com/scummvm/scummvm/commit/a6056fe555cc91aa342c02b1289a3a7a00b0bd6a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-30T18:55:53+02:00

Commit Message:
COLONY: added basic shaders support for 2d primitives

Changed paths:
  A engines/colony/shaders/colony_bitmap.fragment
  A engines/colony/shaders/colony_bitmap.vertex
  A engines/colony/shaders/colony_solid.fragment
  A engines/colony/shaders/colony_solid.vertex
    engines/colony/renderer_opengl.cpp
    engines/colony/renderer_opengl_shaders.cpp
    graphics/opengl/shader.cpp


diff --git a/engines/colony/renderer_opengl.cpp b/engines/colony/renderer_opengl.cpp
index e8a4d56b60c..88fbd364285 100644
--- a/engines/colony/renderer_opengl.cpp
+++ b/engines/colony/renderer_opengl.cpp
@@ -115,6 +115,7 @@ private:
 };
 
 OpenGLRenderer::OpenGLRenderer(OSystem *system, int width, int height) : _system(system), _width(width), _height(height) {
+	debug(1, "Colony: using OpenGL fixed-function renderer");
 	_wireframe = true;
 	_wireframeFillColor = 0;
 	_stippleData = nullptr;
diff --git a/engines/colony/renderer_opengl_shaders.cpp b/engines/colony/renderer_opengl_shaders.cpp
index d7911f51997..378c8383cf2 100644
--- a/engines/colony/renderer_opengl_shaders.cpp
+++ b/engines/colony/renderer_opengl_shaders.cpp
@@ -26,55 +26,49 @@
  */
 
 #include "common/scummsys.h"
+#include "common/config-manager.h"
+#include "common/debug.h"
 #include "common/system.h"
 #include "common/textconsole.h"
+#include "graphics/font.h"
+#include "graphics/surface.h"
+#include "math/matrix4.h"
 
 #include "colony/renderer.h"
 #include "colony/renderer_opengl_shaders.h"
 
 #ifdef USE_OPENGL_SHADERS
 
+#include "graphics/opengl/shader.h"
 #include "graphics/opengl/system_headers.h"
 
 namespace Colony {
 
-// Phase 1: skeleton only. Every Renderer override is a stub. Callers can
-// build with USE_OPENGL_SHADERS, instantiate the renderer (e.g. via
-// `--renderer=opengl_shaders`), but the screen will be empty until later
-// phases fill in primitives, 3D draws, and the surface blit path.
-//
-// The fixed-function path (`renderer_opengl.cpp`) remains the default and
-// is unaffected by this file.
+// Phase 2: programmable-pipeline 2D primitives. The 3D path (begin3D and
+// the corridor draws) and the deprecated state setters (XOR, polygon
+// stipple, wireframe) remain stubs and are filled in by later phases.
 class OpenGLShaderRenderer : public Renderer {
 public:
-	OpenGLShaderRenderer(OSystem *system, int width, int height)
-		: _system(system), _width(width), _height(height) {
-		(void)_width;  // populated for Phase 2+ primitive implementations
-		(void)_height;
-		warning("Colony: OpenGL shader renderer is a Phase 1 skeleton — "
-			"primitives are stubbed; use --renderer=opengl for the working renderer");
-	}
-
-	~OpenGLShaderRenderer() override {}
+	OpenGLShaderRenderer(OSystem *system, int width, int height);
+	~OpenGLShaderRenderer() override;
 
-	// 2D primitives — stubbed.
-	void clear(uint32 color) override {}
-	void drawLine(int x1, int y1, int x2, int y2, uint32 color) override {}
-	void drawRect(const Common::Rect &rect, uint32 color) override {}
-	void fillRect(const Common::Rect &rect, uint32 color) override {}
+	void clear(uint32 color) override;
+	void drawLine(int x1, int y1, int x2, int y2, uint32 color) override;
+	void drawRect(const Common::Rect &rect, uint32 color) override;
+	void fillRect(const Common::Rect &rect, uint32 color) override;
 	void drawString(const Graphics::Font *font, const Common::String &str, int x, int y,
-			uint32 color, Graphics::TextAlign align) override {}
+			uint32 color, Graphics::TextAlign align) override;
 	void scroll(int dx, int dy, uint32 background) override {}
-	void drawEllipse(int x, int y, int rx, int ry, uint32 color) override {}
-	void fillEllipse(int x, int y, int rx, int ry, uint32 color) override {}
-	void fillDitherRect(const Common::Rect &rect, uint32 c1, uint32 c2) override {}
-	void setPixel(int x, int y, uint32 color) override {}
-	void drawQuad(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, uint32 color) override {}
-	void drawPolygon(const int *x, const int *y, int count, uint32 color) override {}
+	void drawEllipse(int x, int y, int rx, int ry, uint32 color) override;
+	void fillEllipse(int x, int y, int rx, int ry, uint32 color) override;
+	void fillDitherRect(const Common::Rect &rect, uint32 c1, uint32 c2) override;
+	void setPixel(int x, int y, uint32 color) override;
+	void drawQuad(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, uint32 color) override;
+	void drawPolygon(const int *x, const int *y, int count, uint32 color) override;
 
-	void setPalette(const byte *palette, uint start, uint count) override {}
+	void setPalette(const byte *palette, uint start, uint count) override;
 
-	// 3D scene rendering — stubbed.
+	// 3D path — Phase 3.
 	void begin3D(int camX, int camY, int camZ, int angle, int angleY, const Common::Rect &viewport) override {}
 	void draw3DWall(int x1, int y1, int x2, int y2, uint32 color) override {}
 	void draw3DQuad(float x1, float y1, float z1, float x2, float y2, float z2,
@@ -83,25 +77,458 @@ public:
 	void draw3DLine(float x1, float y1, float z1, float x2, float y2, float z2, uint32 color) override {}
 	void end3D() override {}
 
-	// Buffer / state management.
-	void copyToScreen() override { _system->updateScreen(); }
+	void copyToScreen() override;
 	void setWireframe(bool enable, int64_t fillColor) override {}
 	void setXorMode(bool enable) override {}
 	void setStippleData(const byte *data) override {}
 	void setMacColors(uint32 fg, uint32 bg) override {}
 	void setDepthState(bool testEnabled, bool writeEnabled) override {}
 	void setDepthRange(float nearVal, float farVal) override {}
-	void computeScreenViewport() override {}
+	void computeScreenViewport() override;
 
-	void drawSurface(const Graphics::Surface *surf, int x, int y) override {}
-	Graphics::Surface *getScreenshot() override { return nullptr; }
+	void drawSurface(const Graphics::Surface *surf, int x, int y) override;
+	Graphics::Surface *getScreenshot() override;
 
 private:
+	void resolveColor(uint32 color, float rgba[4]) const;
+	void rebuildProjection();
+	void uploadSolid(const float *positions, int vertCount);
+	void drawSolid(GLenum mode, const float *positions, int vertCount, const float rgba[4]);
+	void drawTexturedQuad(int x, int y, int w, int h);
+
 	OSystem *_system = nullptr;
 	int _width = 0;
 	int _height = 0;
+	byte _palette[256 * 3] = {};
+	Common::Rect _screenViewport;
+
+	OpenGL::Shader *_solidShader = nullptr;
+	OpenGL::Shader *_bitmapShader = nullptr;
+	GLuint _solidVBO = 0;
+	GLuint _bitmapVBO = 0;
+	GLuint _bitmapTexture = 0;
+
+	Math::Matrix4 _projection;
+
+	// Solid VBO holds vec2 position only. Sized for the worst-case 2D
+	// primitive — the dither overlay can stream up to width*height/2 dots,
+	// so leave room for typical screens (≈170k floats for an 800×600 split).
+	enum { kSolidVertexCapacity = 320 * 1024 };
 };
 
+// ---------------------------------------------------------------------------
+// Construction / teardown
+// ---------------------------------------------------------------------------
+
+OpenGLShaderRenderer::OpenGLShaderRenderer(OSystem *system, int width, int height)
+	: _system(system), _width(width), _height(height) {
+	debug(1, "Colony: using OpenGL shader renderer (Phase 2: 2D primitives "
+		"functional; corridor 3D view is stubbed until Phase 3)");
+	for (int i = 0; i < 256 * 3; i++)
+		_palette[i] = 255;
+
+	static const char *solidAttribs[] = { "position", nullptr };
+	_solidShader = OpenGL::Shader::fromFiles("colony_solid", solidAttribs);
+	_solidVBO = OpenGL::Shader::createBuffer(GL_ARRAY_BUFFER,
+		sizeof(float) * 2 * kSolidVertexCapacity, nullptr, GL_DYNAMIC_DRAW);
+	_solidShader->enableVertexAttribute("position", _solidVBO, 2, GL_FLOAT, GL_FALSE,
+		2 * sizeof(float), 0);
+
+	static const char *bitmapAttribs[] = { "position", "texcoord", nullptr };
+	_bitmapShader = OpenGL::Shader::fromFiles("colony_bitmap", bitmapAttribs);
+	// Per-draw vec2 position + vec2 texcoord, 4 vertices for a quad.
+	_bitmapVBO = OpenGL::Shader::createBuffer(GL_ARRAY_BUFFER,
+		sizeof(float) * 16, nullptr, GL_DYNAMIC_DRAW);
+	_bitmapShader->enableVertexAttribute("position", _bitmapVBO, 2, GL_FLOAT, GL_FALSE,
+		4 * sizeof(float), 0);
+	_bitmapShader->enableVertexAttribute("texcoord", _bitmapVBO, 2, GL_FLOAT, GL_FALSE,
+		4 * sizeof(float), 2 * sizeof(float));
+
+	glGenTextures(1, &_bitmapTexture);
+
+	glDisable(GL_DEPTH_TEST);
+	glDisable(GL_CULL_FACE);
+	glDisable(GL_BLEND);
+
+	computeScreenViewport();
+	rebuildProjection();
+
+	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+	glClear(GL_COLOR_BUFFER_BIT);
+}
+
+OpenGLShaderRenderer::~OpenGLShaderRenderer() {
+	OpenGL::Shader::freeBuffer(_solidVBO);
+	OpenGL::Shader::freeBuffer(_bitmapVBO);
+	delete _solidShader;
+	delete _bitmapShader;
+	if (_bitmapTexture)
+		glDeleteTextures(1, &_bitmapTexture);
+}
+
+// ---------------------------------------------------------------------------
+// Color resolution and matrix setup
+// ---------------------------------------------------------------------------
+
+void OpenGLShaderRenderer::resolveColor(uint32 color, float rgba[4]) const {
+	// Same convention as the fixed-function renderer: high byte 0xFF →
+	// direct ARGB, otherwise palette index (low byte).
+	if (color & 0xFF000000) {
+		rgba[0] = ((color >> 16) & 0xFF) / 255.0f;
+		rgba[1] = ((color >> 8) & 0xFF) / 255.0f;
+		rgba[2] = (color & 0xFF) / 255.0f;
+	} else {
+		const uint32 idx = color & 0xFF;
+		rgba[0] = _palette[idx * 3] / 255.0f;
+		rgba[1] = _palette[idx * 3 + 1] / 255.0f;
+		rgba[2] = _palette[idx * 3 + 2] / 255.0f;
+	}
+	rgba[3] = 1.0f;
+}
+
+void OpenGLShaderRenderer::rebuildProjection() {
+	// Build glOrtho(0, _width, _height, 0, -1, 1) row-major in Math::Matrix4,
+	// then transpose so glUniformMatrix4fv (which reads column-major) sees
+	// the intended matrix. Y is flipped because our engine puts y=0 at top.
+	Math::Matrix4 m;
+	for (int r = 0; r < 4; r++)
+		for (int c = 0; c < 4; c++)
+			m(r, c) = 0.0f;
+	m(0, 0) = 2.0f / (float)_width;
+	m(0, 3) = -1.0f;
+	m(1, 1) = -2.0f / (float)_height;
+	m(1, 3) = 1.0f;
+	m(2, 2) = -1.0f;
+	m(3, 3) = 1.0f;
+	m.transpose();
+	_projection = m;
+}
+
+// ---------------------------------------------------------------------------
+// Solid-color primitives
+// ---------------------------------------------------------------------------
+
+void OpenGLShaderRenderer::uploadSolid(const float *positions, int vertCount) {
+	glBindBuffer(GL_ARRAY_BUFFER, _solidVBO);
+	glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * 2 * vertCount, positions);
+}
+
+void OpenGLShaderRenderer::drawSolid(GLenum mode, const float *positions, int vertCount,
+		const float rgba[4]) {
+	if (vertCount <= 0)
+		return;
+	uploadSolid(positions, vertCount);
+	_solidShader->use();
+	_solidShader->setUniform("projection", _projection);
+	_solidShader->setUniform("color", Math::Vector4d(rgba[0], rgba[1], rgba[2], rgba[3]));
+	glDrawArrays(mode, 0, vertCount);
+	_solidShader->unbind();
+}
+
+void OpenGLShaderRenderer::clear(uint32 color) {
+	float rgba[4];
+	resolveColor(color, rgba);
+	glClearColor(rgba[0], rgba[1], rgba[2], 1.0f);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+}
+
+void OpenGLShaderRenderer::drawLine(int x1, int y1, int x2, int y2, uint32 color) {
+	float rgba[4];
+	resolveColor(color, rgba);
+	const float verts[] = { (float)x1, (float)y1, (float)x2, (float)y2 };
+	drawSolid(GL_LINES, verts, 2, rgba);
+}
+
+void OpenGLShaderRenderer::drawRect(const Common::Rect &rect, uint32 color) {
+	float rgba[4];
+	resolveColor(color, rgba);
+	const float verts[] = {
+		(float)rect.left,  (float)rect.top,
+		(float)rect.right, (float)rect.top,
+		(float)rect.right, (float)rect.bottom,
+		(float)rect.left,  (float)rect.bottom
+	};
+	drawSolid(GL_LINE_LOOP, verts, 4, rgba);
+}
+
+void OpenGLShaderRenderer::fillRect(const Common::Rect &rect, uint32 color) {
+	float rgba[4];
+	resolveColor(color, rgba);
+	// TRIANGLE_STRIP order: top-left, top-right, bottom-left, bottom-right
+	const float verts[] = {
+		(float)rect.left,  (float)rect.top,
+		(float)rect.right, (float)rect.top,
+		(float)rect.left,  (float)rect.bottom,
+		(float)rect.right, (float)rect.bottom
+	};
+	drawSolid(GL_TRIANGLE_STRIP, verts, 4, rgba);
+}
+
+void OpenGLShaderRenderer::setPixel(int x, int y, uint32 color) {
+	// Same as a 1×1 fillRect — this is rarely called now that animation/PICT
+	// blits go through drawSurface.
+	fillRect(Common::Rect(x, y, x + 1, y + 1), color);
+}
+
+void OpenGLShaderRenderer::drawQuad(int x1, int y1, int x2, int y2,
+		int x3, int y3, int x4, int y4, uint32 color) {
+	float rgba[4];
+	resolveColor(color, rgba);
+	// Filled body (TRIANGLE_FAN handles convex quads correctly).
+	const float fanVerts[] = {
+		(float)x1, (float)y1,
+		(float)x2, (float)y2,
+		(float)x3, (float)y3,
+		(float)x4, (float)y4
+	};
+	drawSolid(GL_TRIANGLE_FAN, fanVerts, 4, rgba);
+
+	// Match the fixed-function renderer's white outline overlay.
+	const float white[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
+	drawSolid(GL_LINE_LOOP, fanVerts, 4, white);
+}
+
+void OpenGLShaderRenderer::drawPolygon(const int *x, const int *y, int count, uint32 color) {
+	if (count < 3)
+		return;
+	if (count > 1024)
+		count = 1024;
+
+	float rgba[4];
+	resolveColor(color, rgba);
+
+	float verts[2 * 1024];
+	for (int i = 0; i < count; i++) {
+		verts[i * 2 + 0] = (float)x[i];
+		verts[i * 2 + 1] = (float)y[i];
+	}
+	drawSolid(GL_TRIANGLE_FAN, verts, count, rgba);
+
+	// Same white outline as drawQuad.
+	const float white[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
+	drawSolid(GL_LINE_LOOP, verts, count, white);
+}
+
+void OpenGLShaderRenderer::drawEllipse(int x, int y, int rx, int ry, uint32 color) {
+	float rgba[4];
+	resolveColor(color, rgba);
+	const int kSegments = 36; // 10° steps, matching the fixed-function path
+	float verts[kSegments * 2];
+	for (int i = 0; i < kSegments; i++) {
+		const float rad = i * 2.0f * (float)M_PI / kSegments;
+		verts[i * 2 + 0] = x + cosf(rad) * (float)rx;
+		verts[i * 2 + 1] = y + sinf(rad) * (float)ry;
+	}
+	drawSolid(GL_LINE_LOOP, verts, kSegments, rgba);
+}
+
+void OpenGLShaderRenderer::fillEllipse(int x, int y, int rx, int ry, uint32 color) {
+	float rgba[4];
+	resolveColor(color, rgba);
+	const int kSegments = 36;
+	float verts[kSegments * 2];
+	for (int i = 0; i < kSegments; i++) {
+		const float rad = i * 2.0f * (float)M_PI / kSegments;
+		verts[i * 2 + 0] = x + cosf(rad) * (float)rx;
+		verts[i * 2 + 1] = y + sinf(rad) * (float)ry;
+	}
+	drawSolid(GL_TRIANGLE_FAN, verts, kSegments, rgba);
+}
+
+void OpenGLShaderRenderer::fillDitherRect(const Common::Rect &rect, uint32 c1, uint32 c2) {
+	fillRect(rect, c1);
+	float rgba[4];
+	resolveColor(c2, rgba);
+
+	const int w = rect.width();
+	const int h = rect.height();
+	if (w <= 0 || h <= 0)
+		return;
+
+	// 50% checkerboard: place a dot on every other pixel, alternating per row.
+	// Capacity guard — fall back to solid c2 if the rect is larger than our
+	// streaming buffer (very rare: only the dashboard background hits this
+	// path, and it is much smaller than kSolidVertexCapacity).
+	const int maxDots = kSolidVertexCapacity;
+	int dots = 0;
+	float *verts = new float[maxDots * 2];
+	for (int yi = 0; yi < h && dots < maxDots; yi++) {
+		const int yy = rect.top + yi;
+		for (int xi = (yi & 1); xi < w && dots < maxDots; xi += 2) {
+			verts[dots * 2 + 0] = (float)(rect.left + xi);
+			verts[dots * 2 + 1] = (float)yy;
+			dots++;
+		}
+	}
+	if (dots > 0)
+		drawSolid(GL_POINTS, verts, dots, rgba);
+	delete[] verts;
+}
+
+// ---------------------------------------------------------------------------
+// Text rendering
+// ---------------------------------------------------------------------------
+
+void OpenGLShaderRenderer::drawString(const Graphics::Font *font, const Common::String &str,
+		int x, int y, uint32 color, Graphics::TextAlign align) {
+	if (!font)
+		return;
+	const int w = font->getStringWidth(str);
+	const int h = font->getFontHeight();
+	if (w <= 0 || h <= 0)
+		return;
+
+	if (align == Graphics::kTextAlignCenter)
+		x -= w / 2;
+	else if (align == Graphics::kTextAlignRight)
+		x -= w;
+
+	float rgba[4];
+	resolveColor(color, rgba);
+
+	// Render glyphs to a 1-byte-per-pixel mask, then build an RGBA image
+	// where set pixels carry the requested color (alpha 1) and unset
+	// pixels are transparent. Upload as a texture and draw a single quad.
+	Graphics::Surface mask;
+	mask.create(w, h, Graphics::PixelFormat::createFormatCLUT8());
+	memset(mask.getPixels(), 0, w * h);
+	font->drawString(&mask, str, 0, 0, w, 1, Graphics::kTextAlignLeft);
+
+	const byte cr = (byte)(rgba[0] * 255.0f);
+	const byte cg = (byte)(rgba[1] * 255.0f);
+	const byte cb = (byte)(rgba[2] * 255.0f);
+
+	uint32 *rgbaBuf = new uint32[w * h];
+	for (int py = 0; py < h; py++) {
+		const byte *src = (const byte *)mask.getBasePtr(0, py);
+		uint32 *dst = rgbaBuf + py * w;
+		for (int px = 0; px < w; px++) {
+			if (src[px] == 1)
+				dst[px] = ((uint32)cr) | ((uint32)cg << 8) | ((uint32)cb << 16) | (0xFFu << 24);
+			else
+				dst[px] = 0;
+		}
+	}
+	mask.free();
+
+	glBindTexture(GL_TEXTURE_2D, _bitmapTexture);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgbaBuf);
+	delete[] rgbaBuf;
+
+	glEnable(GL_BLEND);
+	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+	drawTexturedQuad(x, y, w, h);
+	glDisable(GL_BLEND);
+}
+
+// ---------------------------------------------------------------------------
+// Surface blit
+// ---------------------------------------------------------------------------
+
+void OpenGLShaderRenderer::drawSurface(const Graphics::Surface *surf, int x, int y) {
+	if (!surf || surf->w <= 0 || surf->h <= 0)
+		return;
+	if (surf->format.bytesPerPixel != 4)
+		return;
+
+	glBindTexture(GL_TEXTURE_2D, _bitmapTexture);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+	glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
+	// The engine's surface format is PixelFormat(4,8,8,8,8,24,16,8,0) —
+	// R at bit 24, A at bit 0 — so GL_RGBA + GL_UNSIGNED_INT_8_8_8_8 reads
+	// each uint32 with the high byte mapping to R, matching the fixed-
+	// function path's drawSurface upload (renderer_opengl.cpp:725).
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surf->w, surf->h, 0,
+		GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, surf->getPixels());
+
+	glEnable(GL_BLEND);
+	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+	drawTexturedQuad(x, y, surf->w, surf->h);
+	glDisable(GL_BLEND);
+}
+
+void OpenGLShaderRenderer::drawTexturedQuad(int x, int y, int w, int h) {
+	const float verts[] = {
+		// position             texcoord
+		(float)x,       (float)y,       0.0f, 0.0f,
+		(float)(x + w), (float)y,       1.0f, 0.0f,
+		(float)x,       (float)(y + h), 0.0f, 1.0f,
+		(float)(x + w), (float)(y + h), 1.0f, 1.0f
+	};
+	glBindBuffer(GL_ARRAY_BUFFER, _bitmapVBO);
+	glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts);
+
+	_bitmapShader->use();
+	_bitmapShader->setUniform("projection", _projection);
+	_bitmapShader->setUniform("tex", 0);
+	glActiveTexture(GL_TEXTURE0);
+	glBindTexture(GL_TEXTURE_2D, _bitmapTexture);
+
+	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+	_bitmapShader->unbind();
+}
+
+// ---------------------------------------------------------------------------
+// Buffer / state management
+// ---------------------------------------------------------------------------
+
+void OpenGLShaderRenderer::setPalette(const byte *palette, uint start, uint count) {
+	if (start + count > 256)
+		count = 256 - start;
+	memcpy(_palette + start * 3, palette, count * 3);
+}
+
+void OpenGLShaderRenderer::computeScreenViewport() {
+	const int32 screenWidth = _system->getWidth();
+	const int32 screenHeight = _system->getHeight();
+	const bool widescreen = ConfMan.getBool("widescreen_mod");
+
+	if (widescreen) {
+		_screenViewport = Common::Rect(screenWidth, screenHeight);
+	} else if (_system->getFeatureState(OSystem::kFeatureAspectRatioCorrection)) {
+		const int32 vpW = MIN<int32>(screenWidth, screenHeight * 4 / 3);
+		const int32 vpH = MIN<int32>(screenHeight, screenWidth * 3 / 4);
+		_screenViewport = Common::Rect(vpW, vpH);
+		_screenViewport.translate((screenWidth - vpW) / 2, (screenHeight - vpH) / 2);
+	} else {
+		_screenViewport = Common::Rect(screenWidth, screenHeight);
+	}
+
+	glViewport(_screenViewport.left, screenHeight - _screenViewport.bottom,
+		_screenViewport.width(), _screenViewport.height());
+	glScissor(_screenViewport.left, screenHeight - _screenViewport.bottom,
+		_screenViewport.width(), _screenViewport.height());
+}
+
+void OpenGLShaderRenderer::copyToScreen() {
+	glFlush();
+	_system->updateScreen();
+}
+
+Graphics::Surface *OpenGLShaderRenderer::getScreenshot() {
+	Graphics::Surface *surface = new Graphics::Surface();
+	surface->create(_screenViewport.width(), _screenViewport.height(),
+		Graphics::PixelFormat::createFormatRGBA32());
+	glPixelStorei(GL_PACK_ALIGNMENT, 4);
+	glReadPixels(_screenViewport.left, _system->getHeight() - _screenViewport.bottom,
+		_screenViewport.width(), _screenViewport.height(),
+		GL_RGBA, GL_UNSIGNED_BYTE, surface->getPixels());
+	surface->flipVertical(Common::Rect(surface->w, surface->h));
+	return surface;
+}
+
+// ---------------------------------------------------------------------------
+// Factory
+// ---------------------------------------------------------------------------
+
 Renderer *createOpenGLShaderRenderer(OSystem *system, int width, int height) {
 	return new OpenGLShaderRenderer(system, width, height);
 }
diff --git a/engines/colony/shaders/colony_bitmap.fragment b/engines/colony/shaders/colony_bitmap.fragment
new file mode 100644
index 00000000000..0dad76028db
--- /dev/null
+++ b/engines/colony/shaders/colony_bitmap.fragment
@@ -0,0 +1,9 @@
+in vec2 var_texcoord;
+
+OUTPUT
+
+uniform sampler2D tex;
+
+void main() {
+	outColor = texture(tex, var_texcoord);
+}
diff --git a/engines/colony/shaders/colony_bitmap.vertex b/engines/colony/shaders/colony_bitmap.vertex
new file mode 100644
index 00000000000..01d7ea1792b
--- /dev/null
+++ b/engines/colony/shaders/colony_bitmap.vertex
@@ -0,0 +1,11 @@
+in vec2 position;
+in vec2 texcoord;
+
+uniform mat4 projection;
+
+out vec2 var_texcoord;
+
+void main() {
+	var_texcoord = texcoord;
+	gl_Position = projection * vec4(position, 0.0, 1.0);
+}
diff --git a/engines/colony/shaders/colony_solid.fragment b/engines/colony/shaders/colony_solid.fragment
new file mode 100644
index 00000000000..e51434cb4f0
--- /dev/null
+++ b/engines/colony/shaders/colony_solid.fragment
@@ -0,0 +1,7 @@
+OUTPUT
+
+uniform vec4 color;
+
+void main() {
+	outColor = color;
+}
diff --git a/engines/colony/shaders/colony_solid.vertex b/engines/colony/shaders/colony_solid.vertex
new file mode 100644
index 00000000000..5bdb7e3789c
--- /dev/null
+++ b/engines/colony/shaders/colony_solid.vertex
@@ -0,0 +1,7 @@
+in vec2 position;
+
+uniform mat4 projection;
+
+void main() {
+	gl_Position = projection * vec4(position, 0.0, 1.0);
+}
diff --git a/graphics/opengl/shader.cpp b/graphics/opengl/shader.cpp
index f625d2d4ab9..f9985d1fea6 100644
--- a/graphics/opengl/shader.cpp
+++ b/graphics/opengl/shader.cpp
@@ -94,6 +94,7 @@ static const GLchar *readFile(const Common::String &filename) {
 	SearchMan.addDirectory("PLAYGROUND3D_SHADERS", "engines/playground3d", 0, 2);
 	SearchMan.addDirectory("FREESCAPE_SHADERS", "engines/freescape", 0, 2);
 	SearchMan.addDirectory("HPL1_SHADERS", "engines/hpl1/engine/impl", 0, 2);
+	SearchMan.addDirectory("COLONY_SHADERS", "engines/colony", 0, 2);
 #endif
 
 	if (ConfMan.hasKey("extrapath")) {
@@ -114,6 +115,7 @@ static const GLchar *readFile(const Common::String &filename) {
 	SearchMan.remove("PLAYGROUND3D_SHADERS");
 	SearchMan.remove("FREESCAPE_SHADERS");
 	SearchMan.remove("HPL1_SHADERS");
+	SearchMan.remove("COLONY_SHADERS");
 #endif
 
 	SearchMan.remove("EXTRA_PATH");


Commit: e0b482a360dcd0cf875c5d8d3e1495e620ed09c9
    https://github.com/scummvm/scummvm/commit/e0b482a360dcd0cf875c5d8d3e1495e620ed09c9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-30T18:55:53+02:00

Commit Message:
COLONY: added basic shaders support for unoptimized 3d primitives

Changed paths:
  A engines/colony/shaders/colony_solid_3d.vertex
    engines/colony/renderer_opengl_shaders.cpp


diff --git a/engines/colony/renderer_opengl_shaders.cpp b/engines/colony/renderer_opengl_shaders.cpp
index 378c8383cf2..edb247d428a 100644
--- a/engines/colony/renderer_opengl_shaders.cpp
+++ b/engines/colony/renderer_opengl_shaders.cpp
@@ -44,9 +44,9 @@
 
 namespace Colony {
 
-// Phase 2: programmable-pipeline 2D primitives. The 3D path (begin3D and
-// the corridor draws) and the deprecated state setters (XOR, polygon
-// stipple, wireframe) remain stubs and are filled in by later phases.
+// Phase 3: 2D primitives + the 3D corridor draw path are programmable.
+// XOR mode and polygon stipple are intentionally left stubbed — see the
+// renderer audit for why those are deferred.
 class OpenGLShaderRenderer : public Renderer {
 public:
 	OpenGLShaderRenderer(OSystem *system, int width, int height);
@@ -68,22 +68,21 @@ public:
 
 	void setPalette(const byte *palette, uint start, uint count) override;
 
-	// 3D path — Phase 3.
-	void begin3D(int camX, int camY, int camZ, int angle, int angleY, const Common::Rect &viewport) override {}
-	void draw3DWall(int x1, int y1, int x2, int y2, uint32 color) override {}
+	void begin3D(int camX, int camY, int camZ, int angle, int angleY, const Common::Rect &viewport) override;
+	void draw3DWall(int x1, int y1, int x2, int y2, uint32 color) override;
 	void draw3DQuad(float x1, float y1, float z1, float x2, float y2, float z2,
-			float x3, float y3, float z3, float x4, float y4, float z4, uint32 color) override {}
-	void draw3DPolygon(const float *x, const float *y, const float *z, int count, uint32 color) override {}
-	void draw3DLine(float x1, float y1, float z1, float x2, float y2, float z2, uint32 color) override {}
-	void end3D() override {}
+			float x3, float y3, float z3, float x4, float y4, float z4, uint32 color) override;
+	void draw3DPolygon(const float *x, const float *y, const float *z, int count, uint32 color) override;
+	void draw3DLine(float x1, float y1, float z1, float x2, float y2, float z2, uint32 color) override;
+	void end3D() override;
 
 	void copyToScreen() override;
-	void setWireframe(bool enable, int64_t fillColor) override {}
+	void setWireframe(bool enable, int64_t fillColor) override;
 	void setXorMode(bool enable) override {}
 	void setStippleData(const byte *data) override {}
 	void setMacColors(uint32 fg, uint32 bg) override {}
-	void setDepthState(bool testEnabled, bool writeEnabled) override {}
-	void setDepthRange(float nearVal, float farVal) override {}
+	void setDepthState(bool testEnabled, bool writeEnabled) override;
+	void setDepthRange(float nearVal, float farVal) override;
 	void computeScreenViewport() override;
 
 	void drawSurface(const Graphics::Surface *surf, int x, int y) override;
@@ -92,10 +91,20 @@ public:
 private:
 	void resolveColor(uint32 color, float rgba[4]) const;
 	void rebuildProjection();
+	// Push _projection to the 2D shaders. The matrix is constant between
+	// resolution changes, so this only needs to run from the constructor
+	// and computeScreenViewport — not per draw.
+	void uploadProjectionUniform();
 	void uploadSolid(const float *positions, int vertCount);
 	void drawSolid(GLenum mode, const float *positions, int vertCount, const float rgba[4]);
 	void drawTexturedQuad(int x, int y, int w, int h);
 
+	void uploadSolid3D(const float *positions, int vertCount);
+	void drawSolid3D(GLenum mode, const float *positions, int vertCount, const float rgba[4]);
+	// Renders a filled+wireframe 3D primitive. Honors _wireframe and
+	// _wireframeFillColor exactly like OpenGLRenderer::draw3DWall/Quad/Polygon.
+	void drawWireframeable3D(const float *positions, int vertCount, uint32 color);
+
 	OSystem *_system = nullptr;
 	int _width = 0;
 	int _height = 0;
@@ -104,16 +113,25 @@ private:
 
 	OpenGL::Shader *_solidShader = nullptr;
 	OpenGL::Shader *_bitmapShader = nullptr;
+	OpenGL::Shader *_solid3dShader = nullptr;
 	GLuint _solidVBO = 0;
 	GLuint _bitmapVBO = 0;
+	GLuint _solid3dVBO = 0;
 	GLuint _bitmapTexture = 0;
 
 	Math::Matrix4 _projection;
+	Math::Matrix4 _mvpMatrix;
+
+	bool _wireframe = true;
+	int64_t _wireframeFillColor = 0; // -1 = no fill, else color (palette idx or ARGB)
 
 	// Solid VBO holds vec2 position only. Sized for the worst-case 2D
 	// primitive — the dither overlay can stream up to width*height/2 dots,
 	// so leave room for typical screens (≈170k floats for an 800×600 split).
 	enum { kSolidVertexCapacity = 320 * 1024 };
+	// 3D VBO holds vec3 positions. Corridor polygons are typically <16
+	// vertices but a few features (sprite billboards, stairs) push higher.
+	enum { kSolid3DVertexCapacity = 1024 };
 };
 
 // ---------------------------------------------------------------------------
@@ -122,8 +140,7 @@ private:
 
 OpenGLShaderRenderer::OpenGLShaderRenderer(OSystem *system, int width, int height)
 	: _system(system), _width(width), _height(height) {
-	debug(1, "Colony: using OpenGL shader renderer (Phase 2: 2D primitives "
-		"functional; corridor 3D view is stubbed until Phase 3)");
+	debug(1, "Colony: using OpenGL shader renderer");
 	for (int i = 0; i < 256 * 3; i++)
 		_palette[i] = 255;
 
@@ -144,6 +161,15 @@ OpenGLShaderRenderer::OpenGLShaderRenderer(OSystem *system, int width, int heigh
 	_bitmapShader->enableVertexAttribute("texcoord", _bitmapVBO, 2, GL_FLOAT, GL_FALSE,
 		4 * sizeof(float), 2 * sizeof(float));
 
+	// 3D solid: shares colony_solid.fragment (uniform color → outColor) with
+	// the 2D path, but uses a vec3 vertex shader that consumes mvpMatrix.
+	static const char *solid3dAttribs[] = { "position", nullptr };
+	_solid3dShader = OpenGL::Shader::fromFiles("colony_solid_3d", "colony_solid", solid3dAttribs);
+	_solid3dVBO = OpenGL::Shader::createBuffer(GL_ARRAY_BUFFER,
+		sizeof(float) * 3 * kSolid3DVertexCapacity, nullptr, GL_DYNAMIC_DRAW);
+	_solid3dShader->enableVertexAttribute("position", _solid3dVBO, 3, GL_FLOAT, GL_FALSE,
+		3 * sizeof(float), 0);
+
 	glGenTextures(1, &_bitmapTexture);
 
 	glDisable(GL_DEPTH_TEST);
@@ -152,6 +178,12 @@ OpenGLShaderRenderer::OpenGLShaderRenderer(OSystem *system, int width, int heigh
 
 	computeScreenViewport();
 	rebuildProjection();
+	uploadProjectionUniform();
+
+	// The bitmap shader's sampler is permanently bound to texture unit 0.
+	// Setting it once here removes a per-draw setUniform("tex", 0).
+	_bitmapShader->use();
+	_bitmapShader->setUniform("tex", 0);
 
 	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
 	glClear(GL_COLOR_BUFFER_BIT);
@@ -160,8 +192,10 @@ OpenGLShaderRenderer::OpenGLShaderRenderer(OSystem *system, int width, int heigh
 OpenGLShaderRenderer::~OpenGLShaderRenderer() {
 	OpenGL::Shader::freeBuffer(_solidVBO);
 	OpenGL::Shader::freeBuffer(_bitmapVBO);
+	OpenGL::Shader::freeBuffer(_solid3dVBO);
 	delete _solidShader;
 	delete _bitmapShader;
+	delete _solid3dShader;
 	if (_bitmapTexture)
 		glDeleteTextures(1, &_bitmapTexture);
 }
@@ -204,6 +238,20 @@ void OpenGLShaderRenderer::rebuildProjection() {
 	_projection = m;
 }
 
+void OpenGLShaderRenderer::uploadProjectionUniform() {
+	// Push the current ortho matrix to both 2D shaders. The values persist
+	// in each program object until the next setUniform, so subsequent draws
+	// don't need to re-upload it. Color stays per-draw because it varies.
+	if (_solidShader) {
+		_solidShader->use();
+		_solidShader->setUniform("projection", _projection);
+	}
+	if (_bitmapShader) {
+		_bitmapShader->use();
+		_bitmapShader->setUniform("projection", _projection);
+	}
+}
+
 // ---------------------------------------------------------------------------
 // Solid-color primitives
 // ---------------------------------------------------------------------------
@@ -218,11 +266,13 @@ void OpenGLShaderRenderer::drawSolid(GLenum mode, const float *positions, int ve
 	if (vertCount <= 0)
 		return;
 	uploadSolid(positions, vertCount);
+	// "projection" is set once by uploadProjectionUniform() — it persists in
+	// the program object across draws. Only "color" varies per call.
+	// Shader stays bound between draws so Shader::use()'s _previousShader
+	// cache (graphics/opengl/shader.cpp:378-380) skips re-binding.
 	_solidShader->use();
-	_solidShader->setUniform("projection", _projection);
 	_solidShader->setUniform("color", Math::Vector4d(rgba[0], rgba[1], rgba[2], rgba[3]));
 	glDrawArrays(mode, 0, vertCount);
-	_solidShader->unbind();
 }
 
 void OpenGLShaderRenderer::clear(uint32 color) {
@@ -466,14 +516,14 @@ void OpenGLShaderRenderer::drawTexturedQuad(int x, int y, int w, int h) {
 	glBindBuffer(GL_ARRAY_BUFFER, _bitmapVBO);
 	glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts);
 
+	// "projection" and "tex" are set once at init / on resolution change;
+	// they persist in the program object so we don't re-upload here.
 	_bitmapShader->use();
-	_bitmapShader->setUniform("projection", _projection);
-	_bitmapShader->setUniform("tex", 0);
 	glActiveTexture(GL_TEXTURE0);
 	glBindTexture(GL_TEXTURE_2D, _bitmapTexture);
 
 	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
-	_bitmapShader->unbind();
+	// Shader stays bound — see drawSolid for the rationale.
 }
 
 // ---------------------------------------------------------------------------
@@ -525,6 +575,211 @@ Graphics::Surface *OpenGLShaderRenderer::getScreenshot() {
 	return surface;
 }
 
+// ---------------------------------------------------------------------------
+// 3D corridor / scene rendering
+// ---------------------------------------------------------------------------
+
+void OpenGLShaderRenderer::setWireframe(bool enable, int64_t fillColor) {
+	_wireframe = enable;
+	_wireframeFillColor = fillColor;
+}
+
+void OpenGLShaderRenderer::setDepthState(bool testEnabled, bool writeEnabled) {
+	if (testEnabled)
+		glEnable(GL_DEPTH_TEST);
+	else
+		glDisable(GL_DEPTH_TEST);
+	glDepthMask(writeEnabled ? GL_TRUE : GL_FALSE);
+}
+
+void OpenGLShaderRenderer::setDepthRange(float nearVal, float farVal) {
+	glDepthRange(nearVal, farVal);
+}
+
+void OpenGLShaderRenderer::begin3D(int camX, int camY, int camZ, int angle, int angleY,
+		const Common::Rect &viewport) {
+	glEnable(GL_DEPTH_TEST);
+	glClear(GL_DEPTH_BUFFER_BIT);
+
+	// Map the engine's logical viewport into system pixels (matches
+	// OpenGLRenderer::begin3D in renderer_opengl.cpp:246-257).
+	const float scaleX = (float)_screenViewport.width() / (float)_width;
+	const float scaleY = (float)_screenViewport.height() / (float)_height;
+	const int sysH = _system->getHeight();
+	const int vpX = _screenViewport.left + (int)(viewport.left * scaleX);
+	const int vpY = sysH - (_screenViewport.top + (int)(viewport.bottom * scaleY));
+	const int vpW = (int)(viewport.width() * scaleX);
+	const int vpH = (int)(viewport.height() * scaleY);
+	glViewport(vpX, vpY, vpW, vpH);
+	glScissor(vpX, vpY, vpW, vpH);
+	glEnable(GL_SCISSOR_TEST);
+	glDepthFunc(GL_LEQUAL);
+	glDepthMask(GL_TRUE);
+
+	// Perspective: 75° vertical FOV, near=1, far=10000 — matches the
+	// fixed-function path so geometry lands at the same screen positions.
+	const float aspectRatio = (float)viewport.width() / (float)viewport.height();
+	const float fov = 75.0f;
+	const float nearClip = 1.0f;
+	const float farClip = 10000.0f;
+	const float ymax = nearClip * tanf(fov * (float)M_PI / 360.0f);
+	const float xmax = ymax * aspectRatio;
+
+	// Build perspective frustum directly. Math::makeFrustumMatrix exists in
+	// math/glmath.h, but its sign convention requires a final transpose to
+	// match Math::Matrix4's row-major storage; constructing the matrix
+	// element-by-element here is clearer and easier to audit.
+	Math::Matrix4 proj;
+	for (int r = 0; r < 4; r++)
+		for (int c = 0; c < 4; c++)
+			proj(r, c) = 0.0f;
+	proj(0, 0) = 2.0f * nearClip / (xmax - (-xmax));
+	proj(1, 1) = 2.0f * nearClip / (ymax - (-ymax));
+	proj(0, 2) = (xmax + (-xmax)) / (xmax - (-xmax));
+	proj(1, 2) = (ymax + (-ymax)) / (ymax - (-ymax));
+	proj(2, 2) = -(farClip + nearClip) / (farClip - nearClip);
+	proj(2, 3) = -2.0f * farClip * nearClip / (farClip - nearClip);
+	proj(3, 2) = -1.0f;
+
+	// View transform: replicate the fixed-function chain
+	//   Rx(pitch) * Rx(-90) * Rz(yaw) * T(-cam)
+	// applied to column vectors (renderer_opengl.cpp:280-292).
+	Math::Matrix4 pitch;
+	pitch.buildAroundX((float)angleY * 360.0f / 256.0f);
+	Math::Matrix4 minus90;
+	minus90.buildAroundX(-90.0f);
+	Math::Matrix4 yaw;
+	yaw.buildAroundZ(-(float)angle * 360.0f / 256.0f + 90.0f);
+	Math::Matrix4 trans; // identity
+	trans.setPosition(Math::Vector3d((float)-camX, (float)-camY, (float)-camZ));
+
+	const Math::Matrix4 view = pitch * minus90 * yaw * trans;
+	Math::Matrix4 mvp = proj * view;
+	mvp.transpose(); // Math::Matrix4 is row-major; setUniform expects column-major.
+	_mvpMatrix = mvp;
+
+	// Push mvpMatrix to the 3D shader once per frame (here in begin3D),
+	// instead of on every primitive — it doesn't change between draws.
+	_solid3dShader->use();
+	_solid3dShader->setUniform("mvpMatrix", _mvpMatrix);
+}
+
+void OpenGLShaderRenderer::end3D() {
+	glDisable(GL_DEPTH_TEST);
+	glDepthMask(GL_TRUE);
+	glDepthRange(0.0, 1.0);
+	glDisable(GL_SCISSOR_TEST);
+
+	// Restore the 2D viewport so subsequent overlay draws (dashboard, menu,
+	// crosshair, automap) land in the right spot.
+	const int sysW = _system->getWidth();
+	const int sysH = _system->getHeight();
+	glViewport(0, 0, sysW, sysH);
+	glScissor(0, 0, sysW, sysH);
+	computeScreenViewport();
+}
+
+void OpenGLShaderRenderer::uploadSolid3D(const float *positions, int vertCount) {
+	glBindBuffer(GL_ARRAY_BUFFER, _solid3dVBO);
+	glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * 3 * vertCount, positions);
+}
+
+void OpenGLShaderRenderer::drawSolid3D(GLenum mode, const float *positions, int vertCount,
+		const float rgba[4]) {
+	if (vertCount <= 0)
+		return;
+	uploadSolid3D(positions, vertCount);
+	// "mvpMatrix" is set in begin3D and persists in the program object —
+	// only "color" varies per draw. Same shader-bind caching as drawSolid.
+	_solid3dShader->use();
+	_solid3dShader->setUniform("color", Math::Vector4d(rgba[0], rgba[1], rgba[2], rgba[3]));
+	glDrawArrays(mode, 0, vertCount);
+}
+
+void OpenGLShaderRenderer::drawWireframeable3D(const float *positions, int vertCount,
+		uint32 color) {
+	if (vertCount < 3) {
+		// Degenerate — render the line directly so callers don't have to
+		// special-case 2-vertex inputs.
+		float rgba[4];
+		resolveColor(color, rgba);
+		drawSolid3D(GL_LINE_STRIP, positions, vertCount, rgba);
+		return;
+	}
+
+	if (_wireframe) {
+		if (_wireframeFillColor != -1) {
+			glEnable(GL_POLYGON_OFFSET_FILL);
+			glPolygonOffset(1.1f, 4.0f);
+			float fillRgba[4];
+			resolveColor((uint32)_wireframeFillColor, fillRgba);
+			drawSolid3D(GL_TRIANGLE_FAN, positions, vertCount, fillRgba);
+			glDisable(GL_POLYGON_OFFSET_FILL);
+		}
+		float edgeRgba[4];
+		resolveColor(color, edgeRgba);
+		drawSolid3D(GL_LINE_LOOP, positions, vertCount, edgeRgba);
+	} else {
+		glEnable(GL_POLYGON_OFFSET_FILL);
+		glPolygonOffset(1.1f, 4.0f);
+		float rgba[4];
+		resolveColor(color, rgba);
+		drawSolid3D(GL_TRIANGLE_FAN, positions, vertCount, rgba);
+		glDisable(GL_POLYGON_OFFSET_FILL);
+	}
+}
+
+void OpenGLShaderRenderer::draw3DWall(int x1, int y1, int x2, int y2, uint32 color) {
+	// 256× scale and ±160 height match renderer_opengl.cpp:295-298.
+	const float fx1 = x1 * 256.0f, fy1 = y1 * 256.0f;
+	const float fx2 = x2 * 256.0f, fy2 = y2 * 256.0f;
+	const float verts[12] = {
+		fx1, fy1, -160.0f,
+		fx2, fy2, -160.0f,
+		fx2, fy2,  160.0f,
+		fx1, fy1,  160.0f,
+	};
+	drawWireframeable3D(verts, 4, color);
+}
+
+void OpenGLShaderRenderer::draw3DQuad(float x1, float y1, float z1, float x2, float y2, float z2,
+		float x3, float y3, float z3, float x4, float y4, float z4, uint32 color) {
+	const float verts[12] = {
+		x1, y1, z1,
+		x2, y2, z2,
+		x3, y3, z3,
+		x4, y4, z4,
+	};
+	drawWireframeable3D(verts, 4, color);
+}
+
+void OpenGLShaderRenderer::draw3DPolygon(const float *x, const float *y, const float *z,
+		int count, uint32 color) {
+	if (count < 3)
+		return;
+	if (count > kSolid3DVertexCapacity)
+		count = kSolid3DVertexCapacity;
+
+	float stack[3 * 32];
+	float *verts = (count <= 32) ? stack : new float[3 * count];
+	for (int i = 0; i < count; i++) {
+		verts[i * 3 + 0] = x[i];
+		verts[i * 3 + 1] = y[i];
+		verts[i * 3 + 2] = z[i];
+	}
+	drawWireframeable3D(verts, count, color);
+	if (verts != stack)
+		delete[] verts;
+}
+
+void OpenGLShaderRenderer::draw3DLine(float x1, float y1, float z1, float x2, float y2, float z2,
+		uint32 color) {
+	const float verts[6] = { x1, y1, z1, x2, y2, z2 };
+	float rgba[4];
+	resolveColor(color, rgba);
+	drawSolid3D(GL_LINES, verts, 2, rgba);
+}
+
 // ---------------------------------------------------------------------------
 // Factory
 // ---------------------------------------------------------------------------
diff --git a/engines/colony/shaders/colony_solid_3d.vertex b/engines/colony/shaders/colony_solid_3d.vertex
new file mode 100644
index 00000000000..4826ea4c434
--- /dev/null
+++ b/engines/colony/shaders/colony_solid_3d.vertex
@@ -0,0 +1,7 @@
+in vec3 position;
+
+uniform mat4 mvpMatrix;
+
+void main() {
+	gl_Position = mvpMatrix * vec4(position, 1.0);
+}


Commit: 47442da883f1f6b242f4cd26f7f9a0b94a7d3807
    https://github.com/scummvm/scummvm/commit/47442da883f1f6b242f4cd26f7f9a0b94a7d3807
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-30T18:55:53+02:00

Commit Message:
COLONY: optimize shader 3d primitives code

Changed paths:
    engines/colony/renderer_opengl_shaders.cpp


diff --git a/engines/colony/renderer_opengl_shaders.cpp b/engines/colony/renderer_opengl_shaders.cpp
index edb247d428a..dc2696745b2 100644
--- a/engines/colony/renderer_opengl_shaders.cpp
+++ b/engines/colony/renderer_opengl_shaders.cpp
@@ -122,6 +122,14 @@ private:
 	Math::Matrix4 _projection;
 	Math::Matrix4 _mvpMatrix;
 
+	// Cached "color" uniform values per shader. Uniforms persist with the
+	// program object across glUseProgram cycles, so once we've uploaded a
+	// color, subsequent draws can skip glUniform4fv when the color matches.
+	// Adjacent walls / dashboard primitives commonly share colors, so this
+	// elides a lot of uniform writes per frame.
+	float _solidLastColor[4] = { -1.0f, -1.0f, -1.0f, -1.0f };
+	float _solid3dLastColor[4] = { -1.0f, -1.0f, -1.0f, -1.0f };
+
 	bool _wireframe = true;
 	int64_t _wireframeFillColor = 0; // -1 = no fill, else color (palette idx or ARGB)
 
@@ -171,6 +179,15 @@ OpenGLShaderRenderer::OpenGLShaderRenderer(OSystem *system, int width, int heigh
 		3 * sizeof(float), 0);
 
 	glGenTextures(1, &_bitmapTexture);
+	// Texture parameters are per-texture-object state, so they only need to
+	// be set once at creation; they persist across glTexImage2D re-uploads.
+	// All blits (drawSurface, drawString) reuse this single texture handle
+	// and share the same NEAREST filter + CLAMP_TO_EDGE wrap.
+	glBindTexture(GL_TEXTURE_2D, _bitmapTexture);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 
 	glDisable(GL_DEPTH_TEST);
 	glDisable(GL_CULL_FACE);
@@ -257,8 +274,13 @@ void OpenGLShaderRenderer::uploadProjectionUniform() {
 // ---------------------------------------------------------------------------
 
 void OpenGLShaderRenderer::uploadSolid(const float *positions, int vertCount) {
+	// glBufferData (orphan) instead of glBufferSubData: tells the driver
+	// the previous contents are dead, so it can hand us fresh storage
+	// without waiting for the GPU to finish reading the old data. This
+	// matches Freescape's per-draw pattern (gfx_opengl_shaders.cpp:695,
+	// 717) and avoids implicit CPU/GPU sync stalls on Mac drivers.
 	glBindBuffer(GL_ARRAY_BUFFER, _solidVBO);
-	glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * 2 * vertCount, positions);
+	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 2 * vertCount, positions, GL_DYNAMIC_DRAW);
 }
 
 void OpenGLShaderRenderer::drawSolid(GLenum mode, const float *positions, int vertCount,
@@ -267,11 +289,16 @@ void OpenGLShaderRenderer::drawSolid(GLenum mode, const float *positions, int ve
 		return;
 	uploadSolid(positions, vertCount);
 	// "projection" is set once by uploadProjectionUniform() — it persists in
-	// the program object across draws. Only "color" varies per call.
-	// Shader stays bound between draws so Shader::use()'s _previousShader
-	// cache (graphics/opengl/shader.cpp:378-380) skips re-binding.
+	// the program object across draws. Shader stays bound between draws so
+	// Shader::use()'s _previousShader cache short-circuits the rebind.
 	_solidShader->use();
-	_solidShader->setUniform("color", Math::Vector4d(rgba[0], rgba[1], rgba[2], rgba[3]));
+	if (rgba[0] != _solidLastColor[0] || rgba[1] != _solidLastColor[1]
+			|| rgba[2] != _solidLastColor[2] || rgba[3] != _solidLastColor[3]) {
+		_solidShader->setUniform("color",
+			Math::Vector4d(rgba[0], rgba[1], rgba[2], rgba[3]));
+		_solidLastColor[0] = rgba[0]; _solidLastColor[1] = rgba[1];
+		_solidLastColor[2] = rgba[2]; _solidLastColor[3] = rgba[3];
+	}
 	glDrawArrays(mode, 0, vertCount);
 }
 
@@ -461,11 +488,9 @@ void OpenGLShaderRenderer::drawString(const Graphics::Font *font, const Common::
 	}
 	mask.free();
 
+	// Texture params are set once at construction. The shared _bitmapTexture
+	// keeps NEAREST filter + CLAMP_TO_EDGE for both drawString and drawSurface.
 	glBindTexture(GL_TEXTURE_2D, _bitmapTexture);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
 	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgbaBuf);
 	delete[] rgbaBuf;
@@ -486,11 +511,8 @@ void OpenGLShaderRenderer::drawSurface(const Graphics::Surface *surf, int x, int
 	if (surf->format.bytesPerPixel != 4)
 		return;
 
+	// Texture params are set once at construction; just bind here.
 	glBindTexture(GL_TEXTURE_2D, _bitmapTexture);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 	glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
 	// The engine's surface format is PixelFormat(4,8,8,8,8,24,16,8,0) —
 	// R at bit 24, A at bit 0 — so GL_RGBA + GL_UNSIGNED_INT_8_8_8_8 reads
@@ -513,8 +535,9 @@ void OpenGLShaderRenderer::drawTexturedQuad(int x, int y, int w, int h) {
 		(float)x,       (float)(y + h), 0.0f, 1.0f,
 		(float)(x + w), (float)(y + h), 1.0f, 1.0f
 	};
+	// Orphan + write fresh — see uploadSolid.
 	glBindBuffer(GL_ARRAY_BUFFER, _bitmapVBO);
-	glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts);
+	glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_DYNAMIC_DRAW);
 
 	// "projection" and "tex" are set once at init / on resolution change;
 	// they persist in the program object so we don't re-upload here.
@@ -680,8 +703,9 @@ void OpenGLShaderRenderer::end3D() {
 }
 
 void OpenGLShaderRenderer::uploadSolid3D(const float *positions, int vertCount) {
+	// See uploadSolid for the orphan-via-glBufferData rationale.
 	glBindBuffer(GL_ARRAY_BUFFER, _solid3dVBO);
-	glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * 3 * vertCount, positions);
+	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * vertCount, positions, GL_DYNAMIC_DRAW);
 }
 
 void OpenGLShaderRenderer::drawSolid3D(GLenum mode, const float *positions, int vertCount,
@@ -689,10 +713,17 @@ void OpenGLShaderRenderer::drawSolid3D(GLenum mode, const float *positions, int
 	if (vertCount <= 0)
 		return;
 	uploadSolid3D(positions, vertCount);
-	// "mvpMatrix" is set in begin3D and persists in the program object —
-	// only "color" varies per draw. Same shader-bind caching as drawSolid.
+	// "mvpMatrix" is set in begin3D and persists in the program object.
+	// Skip the color upload when it matches the previous draw — adjacent
+	// corridor walls/quads commonly share a color.
 	_solid3dShader->use();
-	_solid3dShader->setUniform("color", Math::Vector4d(rgba[0], rgba[1], rgba[2], rgba[3]));
+	if (rgba[0] != _solid3dLastColor[0] || rgba[1] != _solid3dLastColor[1]
+			|| rgba[2] != _solid3dLastColor[2] || rgba[3] != _solid3dLastColor[3]) {
+		_solid3dShader->setUniform("color",
+			Math::Vector4d(rgba[0], rgba[1], rgba[2], rgba[3]));
+		_solid3dLastColor[0] = rgba[0]; _solid3dLastColor[1] = rgba[1];
+		_solid3dLastColor[2] = rgba[2]; _solid3dLastColor[3] = rgba[3];
+	}
 	glDrawArrays(mode, 0, vertCount);
 }
 


Commit: 22276d5926b57201928f086f2e80c7dd5c9ebc11
    https://github.com/scummvm/scummvm/commit/22276d5926b57201928f086f2e80c7dd5c9ebc11
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-30T18:55:53+02:00

Commit Message:
COLONY: support for stipples in shader renderer

Changed paths:
  A engines/colony/shaders/colony_solid_3d.fragment
    engines/colony/renderer_opengl_shaders.cpp


diff --git a/engines/colony/renderer_opengl_shaders.cpp b/engines/colony/renderer_opengl_shaders.cpp
index dc2696745b2..4db4864e2b5 100644
--- a/engines/colony/renderer_opengl_shaders.cpp
+++ b/engines/colony/renderer_opengl_shaders.cpp
@@ -79,8 +79,8 @@ public:
 	void copyToScreen() override;
 	void setWireframe(bool enable, int64_t fillColor) override;
 	void setXorMode(bool enable) override {}
-	void setStippleData(const byte *data) override {}
-	void setMacColors(uint32 fg, uint32 bg) override {}
+	void setStippleData(const byte *data) override;
+	void setMacColors(uint32 fg, uint32 bg) override;
 	void setDepthState(bool testEnabled, bool writeEnabled) override;
 	void setDepthRange(float nearVal, float farVal) override;
 	void computeScreenViewport() override;
@@ -100,9 +100,14 @@ private:
 	void drawTexturedQuad(int x, int y, int w, int h);
 
 	void uploadSolid3D(const float *positions, int vertCount);
-	void drawSolid3D(GLenum mode, const float *positions, int vertCount, const float rgba[4]);
-	// Renders a filled+wireframe 3D primitive. Honors _wireframe and
-	// _wireframeFillColor exactly like OpenGLRenderer::draw3DWall/Quad/Polygon.
+	// allowStipple=true on the fill pass picks up _stippleActive; lines
+	// must always render unstippled (matches the fixed-function path,
+	// which only stipples GL_QUADS / GL_POLYGON, never lines).
+	void drawSolid3D(GLenum mode, const float *positions, int vertCount,
+			const float rgba[4], bool allowStipple = false);
+	// Renders a filled+wireframe 3D primitive. Honors _wireframe,
+	// _wireframeFillColor, and _stippleActive — same semantics as
+	// OpenGLRenderer::draw3DWall/Quad/Polygon.
 	void drawWireframeable3D(const float *positions, int vertCount, uint32 color);
 
 	OSystem *_system = nullptr;
@@ -133,6 +138,29 @@ private:
 	bool _wireframe = true;
 	int64_t _wireframeFillColor = 0; // -1 = no fill, else color (palette idx or ARGB)
 
+	// Stipple state. The shader emulates glPolygonStipple via a 128-int
+	// uniform array (Freescape pattern, GLES2-safe). _stippleActive is
+	// true when setStippleData() received a non-null pattern. fg/bg are
+	// the engine's encoded colors (high byte 0xFF = direct ARGB, else
+	// palette index); they are resolved to vec4 before upload.
+	//
+	// Defaults match OpenGLRenderer (fg = palette index 0 = black,
+	// bg = palette index 255 = white). B&W Mac mode never calls
+	// setMacColors and relies on these defaults.
+	int _stippleShaderArray[128] = {};
+	bool _stippleActive = false;
+	uint32 _stippleFg = 0;
+	uint32 _stippleBg = 255;
+	// Per-shader dirty flags. Track what the program object currently has
+	// so we can skip redundant uniform uploads (same idea as the color
+	// cache). _solid3dStippleEnabled = the value of "useStipple" most
+	// recently uploaded to _solid3dShader. _stippleColorsDirty starts true
+	// so the first stipple draw uploads the resolved colors even if the
+	// engine never calls setMacColors (the B&W Mac path).
+	bool _solid3dStippleEnabled = false;
+	bool _stipplePatternDirty = false;
+	bool _stippleColorsDirty = true;
+
 	// Solid VBO holds vec2 position only. Sized for the worst-case 2D
 	// primitive — the dither overlay can stream up to width*height/2 dots,
 	// so leave room for typical screens (≈170k floats for an 800×600 split).
@@ -169,14 +197,19 @@ OpenGLShaderRenderer::OpenGLShaderRenderer(OSystem *system, int width, int heigh
 	_bitmapShader->enableVertexAttribute("texcoord", _bitmapVBO, 2, GL_FLOAT, GL_FALSE,
 		4 * sizeof(float), 2 * sizeof(float));
 
-	// 3D solid: shares colony_solid.fragment (uniform color → outColor) with
-	// the 2D path, but uses a vec3 vertex shader that consumes mvpMatrix.
+	// 3D solid: vec3 vertex consuming mvpMatrix; the fragment shader has
+	// its own stipple-emulation branch (Freescape pattern, GLES2 safe),
+	// so we use a dedicated colony_solid_3d.{vertex,fragment} pair.
 	static const char *solid3dAttribs[] = { "position", nullptr };
-	_solid3dShader = OpenGL::Shader::fromFiles("colony_solid_3d", "colony_solid", solid3dAttribs);
+	_solid3dShader = OpenGL::Shader::fromFiles("colony_solid_3d", solid3dAttribs);
 	_solid3dVBO = OpenGL::Shader::createBuffer(GL_ARRAY_BUFFER,
 		sizeof(float) * 3 * kSolid3DVertexCapacity, nullptr, GL_DYNAMIC_DRAW);
 	_solid3dShader->enableVertexAttribute("position", _solid3dVBO, 3, GL_FLOAT, GL_FALSE,
 		3 * sizeof(float), 0);
+	// Initial stipple state: disabled. The fragment shader takes the
+	// non-stippled path until setStippleData(non-null) marks otherwise.
+	_solid3dShader->use();
+	_solid3dShader->setUniform("useStipple", 0u);
 
 	glGenTextures(1, &_bitmapTexture);
 	// Texture parameters are per-texture-object state, so they only need to
@@ -607,6 +640,26 @@ void OpenGLShaderRenderer::setWireframe(bool enable, int64_t fillColor) {
 	_wireframeFillColor = fillColor;
 }
 
+void OpenGLShaderRenderer::setStippleData(const byte *data) {
+	const bool nowActive = (data != nullptr);
+	if (nowActive) {
+		// Widen the 128 pattern bytes into ints for the uniform array.
+		// Mark the pattern dirty so the next 3D draw uploads it.
+		for (int i = 0; i < 128; i++)
+			_stippleShaderArray[i] = data[i];
+		_stipplePatternDirty = true;
+	}
+	_stippleActive = nowActive;
+}
+
+void OpenGLShaderRenderer::setMacColors(uint32 fg, uint32 bg) {
+	if (_stippleFg != fg || _stippleBg != bg) {
+		_stippleFg = fg;
+		_stippleBg = bg;
+		_stippleColorsDirty = true;
+	}
+}
+
 void OpenGLShaderRenderer::setDepthState(bool testEnabled, bool writeEnabled) {
 	if (testEnabled)
 		glEnable(GL_DEPTH_TEST);
@@ -709,20 +762,51 @@ void OpenGLShaderRenderer::uploadSolid3D(const float *positions, int vertCount)
 }
 
 void OpenGLShaderRenderer::drawSolid3D(GLenum mode, const float *positions, int vertCount,
-		const float rgba[4]) {
+		const float rgba[4], bool allowStipple) {
 	if (vertCount <= 0)
 		return;
 	uploadSolid3D(positions, vertCount);
 	// "mvpMatrix" is set in begin3D and persists in the program object.
-	// Skip the color upload when it matches the previous draw — adjacent
-	// corridor walls/quads commonly share a color.
 	_solid3dShader->use();
-	if (rgba[0] != _solid3dLastColor[0] || rgba[1] != _solid3dLastColor[1]
-			|| rgba[2] != _solid3dLastColor[2] || rgba[3] != _solid3dLastColor[3]) {
-		_solid3dShader->setUniform("color",
-			Math::Vector4d(rgba[0], rgba[1], rgba[2], rgba[3]));
-		_solid3dLastColor[0] = rgba[0]; _solid3dLastColor[1] = rgba[1];
-		_solid3dLastColor[2] = rgba[2]; _solid3dLastColor[3] = rgba[3];
+
+	// Toggle the shader's stipple branch only when the effective state
+	// changes. allowStipple=false short-circuits stipple for line passes.
+	const bool wantStipple = allowStipple && _stippleActive;
+	if (wantStipple != _solid3dStippleEnabled) {
+		_solid3dShader->setUniform("useStipple", wantStipple ? 1u : 0u);
+		_solid3dStippleEnabled = wantStipple;
+	}
+
+	if (wantStipple) {
+		// Pattern + fg/bg colors are uploaded only when they change.
+		if (_stipplePatternDirty) {
+			_solid3dShader->setUniform("stipple", 128, _stippleShaderArray);
+			_stipplePatternDirty = false;
+		}
+		if (_stippleColorsDirty) {
+			float fgRgba[4], bgRgba[4];
+			resolveColor(_stippleFg, fgRgba);
+			resolveColor(_stippleBg, bgRgba);
+			_solid3dShader->setUniform("stippleFg",
+				Math::Vector4d(fgRgba[0], fgRgba[1], fgRgba[2], fgRgba[3]));
+			_solid3dShader->setUniform("stippleBg",
+				Math::Vector4d(bgRgba[0], bgRgba[1], bgRgba[2], bgRgba[3]));
+			_stippleColorsDirty = false;
+		}
+		// "color" uniform isn't sampled when useStipple is set — skip the
+		// upload entirely. Invalidate the cache so a later non-stipple
+		// draw with the same rgba still uploads.
+		_solid3dLastColor[0] = -1.0f;
+	} else {
+		// Skip the color upload when it matches the previous draw —
+		// adjacent corridor walls/quads commonly share a color.
+		if (rgba[0] != _solid3dLastColor[0] || rgba[1] != _solid3dLastColor[1]
+				|| rgba[2] != _solid3dLastColor[2] || rgba[3] != _solid3dLastColor[3]) {
+			_solid3dShader->setUniform("color",
+				Math::Vector4d(rgba[0], rgba[1], rgba[2], rgba[3]));
+			_solid3dLastColor[0] = rgba[0]; _solid3dLastColor[1] = rgba[1];
+			_solid3dLastColor[2] = rgba[2]; _solid3dLastColor[3] = rgba[3];
+		}
 	}
 	glDrawArrays(mode, 0, vertCount);
 }
@@ -739,23 +823,27 @@ void OpenGLShaderRenderer::drawWireframeable3D(const float *positions, int vertC
 	}
 
 	if (_wireframe) {
-		if (_wireframeFillColor != -1) {
+		// Fill pass: stipple beats wireframeFillColor when active. Pass
+		// allowStipple=true so drawSolid3D picks up _stippleActive and
+		// uses the fg/bg uniforms; the fill color is ignored in that case.
+		if (_stippleActive || _wireframeFillColor != -1) {
 			glEnable(GL_POLYGON_OFFSET_FILL);
 			glPolygonOffset(1.1f, 4.0f);
 			float fillRgba[4];
-			resolveColor((uint32)_wireframeFillColor, fillRgba);
-			drawSolid3D(GL_TRIANGLE_FAN, positions, vertCount, fillRgba);
+			resolveColor(_stippleActive ? 0u : (uint32)_wireframeFillColor, fillRgba);
+			drawSolid3D(GL_TRIANGLE_FAN, positions, vertCount, fillRgba, true);
 			glDisable(GL_POLYGON_OFFSET_FILL);
 		}
+		// Edges: lines are never stippled in the fixed-function path.
 		float edgeRgba[4];
 		resolveColor(color, edgeRgba);
-		drawSolid3D(GL_LINE_LOOP, positions, vertCount, edgeRgba);
+		drawSolid3D(GL_LINE_LOOP, positions, vertCount, edgeRgba, false);
 	} else {
 		glEnable(GL_POLYGON_OFFSET_FILL);
 		glPolygonOffset(1.1f, 4.0f);
 		float rgba[4];
 		resolveColor(color, rgba);
-		drawSolid3D(GL_TRIANGLE_FAN, positions, vertCount, rgba);
+		drawSolid3D(GL_TRIANGLE_FAN, positions, vertCount, rgba, true);
 		glDisable(GL_POLYGON_OFFSET_FILL);
 	}
 }
diff --git a/engines/colony/shaders/colony_solid_3d.fragment b/engines/colony/shaders/colony_solid_3d.fragment
new file mode 100644
index 00000000000..0bedb43516d
--- /dev/null
+++ b/engines/colony/shaders/colony_solid_3d.fragment
@@ -0,0 +1,49 @@
+OUTPUT
+
+uniform vec4 color;
+
+// Stipple state. The pattern is the same 32×32 / 128-byte layout that
+// glPolygonStipple expects, encoded as 128 ints. Bit 1 → foreground,
+// bit 0 → background (Mac QuickDraw convention).
+//
+// Bit-extraction logic mirrors Freescape's freescape_triangle.fragment:
+// constant-iteration loops avoid GLES2's restriction on dynamic indexing
+// of uniform arrays.
+uniform UBOOL useStipple;
+uniform int stipple[128];
+uniform vec4 stippleFg;
+uniform vec4 stippleBg;
+
+void main() {
+	if (UBOOL_TEST(useStipple)) {
+		ivec2 coord = ivec2(gl_FragCoord.xy);
+		int x = int(mod(float(coord.x), 32.));
+		int y = int(mod(float(coord.y), 32.));
+
+		// Each row has 4 bytes (4 × 8 = 32 columns).
+		int byteIndex = y * 4 + (x / 8);
+		int bitIndex = int(mod(float(x), 8.));
+
+		int patternByte = 0;
+		for (int i = 0; i < 128; i++) {
+			if (i == byteIndex) {
+				patternByte = stipple[i];
+				break;
+			}
+		}
+
+		// Bit 7 (MSB) = leftmost column. Right-shift (7 - bitIndex) times.
+		for (int i = 0; i < 7; i++) {
+			if (i >= 7 - bitIndex)
+				break;
+			patternByte = patternByte / 2;
+		}
+
+		if (int(mod(float(patternByte), 2.)) == 0)
+			outColor = stippleBg;
+		else
+			outColor = stippleFg;
+	} else {
+		outColor = color;
+	}
+}


Commit: 4f4b855c9df3bb89fd3699c791698fb62352b223
    https://github.com/scummvm/scummvm/commit/4f4b855c9df3bb89fd3699c791698fb62352b223
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-30T18:55:53+02:00

Commit Message:
COLONY: add shader files into distribution files

Changed paths:
    Makefile.common
    devtools/create_project/xcode.cpp


diff --git a/Makefile.common b/Makefile.common
index 21b7e4392fe..12074f25763 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -459,6 +459,9 @@ endif
 ifdef ENABLE_FREESCAPE
 DIST_FILES_SHADERS+=$(wildcard $(srcdir)/engines/freescape/shaders/*)
 endif
+ifdef ENABLE_COLONY
+DIST_FILES_SHADERS+=$(wildcard $(srcdir)/engines/colony/shaders/*)
+endif
 endif
 
 # Soundfonts
diff --git a/devtools/create_project/xcode.cpp b/devtools/create_project/xcode.cpp
index d4d3b547c36..20779d3ab8d 100644
--- a/devtools/create_project/xcode.cpp
+++ b/devtools/create_project/xcode.cpp
@@ -1141,6 +1141,14 @@ XcodeProvider::ValueList& XcodeProvider::getResourceFiles(const BuildSetup &setu
 			files.push_back("engines/freescape/shaders/freescape_cubemap.fragment");
 			files.push_back("engines/freescape/shaders/freescape_cubemap.vertex");
 		}
+		if (CONTAINS_DEFINE(setup.defines, "ENABLE_COLONY")) {
+			files.push_back("engines/colony/shaders/colony_solid.fragment");
+			files.push_back("engines/colony/shaders/colony_solid.vertex");
+			files.push_back("engines/colony/shaders/colony_solid_3d.fragment");
+			files.push_back("engines/colony/shaders/colony_solid_3d.vertex");
+			files.push_back("engines/colony/shaders/colony_bitmap.fragment");
+			files.push_back("engines/colony/shaders/colony_bitmap.vertex");
+		}
 		if (CONTAINS_DEFINE(setup.defines, "USE_FLUIDSYNTH")) {
 			files.push_back("dists/soundfonts/Roland_SC-55.sf2");
 			files.push_back("dists/soundfonts/COPYRIGHT.Roland_SC-55");


Commit: ca3f853772918ab622b5f741b47dbe25296b1734
    https://github.com/scummvm/scummvm/commit/ca3f853772918ab622b5f741b47dbe25296b1734
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-30T18:55:53+02:00

Commit Message:
COLONY: deleted scroll interface

Changed paths:
    engines/colony/renderer.h
    engines/colony/renderer_opengl.cpp
    engines/colony/renderer_opengl_shaders.cpp


diff --git a/engines/colony/renderer.h b/engines/colony/renderer.h
index fc9fb696c7a..8145baa0f40 100644
--- a/engines/colony/renderer.h
+++ b/engines/colony/renderer.h
@@ -45,7 +45,6 @@ public:
 	virtual void drawRect(const Common::Rect &rect, uint32 color) = 0;
 	virtual void fillRect(const Common::Rect &rect, uint32 color) = 0;
 	virtual void drawString(const Graphics::Font *font, const Common::String &str, int x, int y, uint32 color, Graphics::TextAlign align = Graphics::kTextAlignLeft) = 0;
-	virtual void scroll(int dx, int dy, uint32 background) = 0;
 	virtual void drawEllipse(int x, int y, int rx, int ry, uint32 color) = 0;
 	virtual void fillEllipse(int x, int y, int rx, int ry, uint32 color) = 0;
 	virtual void fillDitherRect(const Common::Rect &rect, uint32 color1, uint32 color2) = 0;
diff --git a/engines/colony/renderer_opengl.cpp b/engines/colony/renderer_opengl.cpp
index 88fbd364285..6c1bf0cfd0b 100644
--- a/engines/colony/renderer_opengl.cpp
+++ b/engines/colony/renderer_opengl.cpp
@@ -48,7 +48,6 @@ public:
 	void drawRect(const Common::Rect &rect, uint32 color) override;
 	void fillRect(const Common::Rect &rect, uint32 color) override;
 	void drawString(const Graphics::Font *font, const Common::String &str, int x, int y, uint32 color, Graphics::TextAlign align) override;
-	void scroll(int dx, int dy, uint32 background) override;
 	void drawEllipse(int x, int y, int rx, int ry, uint32 color) override;
 	void fillEllipse(int x, int y, int rx, int ry, uint32 color) override;
 	void fillDitherRect(const Common::Rect &rect, uint32 color1, uint32 color2) override;
@@ -554,9 +553,6 @@ void OpenGLRenderer::computeScreenViewport() {
 	glScissor(_screenViewport.left, screenHeight - _screenViewport.bottom, _screenViewport.width(), _screenViewport.height());
 }
 
-void OpenGLRenderer::scroll(int dx, int dy, uint32 background) {
-}
-
 void OpenGLRenderer::drawEllipse(int x, int y, int rx, int ry, uint32 color) {
 	GLint savedViewport[4];
 	GLint savedScissor[4];
diff --git a/engines/colony/renderer_opengl_shaders.cpp b/engines/colony/renderer_opengl_shaders.cpp
index 4db4864e2b5..cf7ed31d02c 100644
--- a/engines/colony/renderer_opengl_shaders.cpp
+++ b/engines/colony/renderer_opengl_shaders.cpp
@@ -58,7 +58,6 @@ public:
 	void fillRect(const Common::Rect &rect, uint32 color) override;
 	void drawString(const Graphics::Font *font, const Common::String &str, int x, int y,
 			uint32 color, Graphics::TextAlign align) override;
-	void scroll(int dx, int dy, uint32 background) override {}
 	void drawEllipse(int x, int y, int rx, int ry, uint32 color) override;
 	void fillEllipse(int x, int y, int rx, int ry, uint32 color) override;
 	void fillDitherRect(const Common::Rect &rect, uint32 c1, uint32 c2) override;


Commit: c66e0f4fa3da891ee9615565077bb6890276a8be
    https://github.com/scummvm/scummvm/commit/c66e0f4fa3da891ee9615565077bb6890276a8be
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-30T18:55:53+02:00

Commit Message:
COLONY: enable SupportsArbitraryResolutions

Changed paths:
    engines/colony/animation.cpp
    engines/colony/colony.cpp
    engines/colony/colony.h
    engines/colony/intro.cpp
    engines/colony/renderer_opengl.cpp
    engines/colony/renderer_opengl_shaders.cpp
    engines/colony/savegame.cpp


diff --git a/engines/colony/animation.cpp b/engines/colony/animation.cpp
index 970fe084230..93eeba9e536 100644
--- a/engines/colony/animation.cpp
+++ b/engines/colony/animation.cpp
@@ -397,7 +397,7 @@ void ColonyEngine::playAnimation() {
 	_animationRunning = true;
 	_system->lockMouse(false);
 	_system->showMouse(true);
-	_system->warpMouse(_centerX, _centerY);
+	warpMouseLogical(_centerX, _centerY);
 	const char *cursorName = "default arrow cursor";
 	if (_renderMode == Common::kRenderMacintosh && _macArrowCursor) {
 		cursorName = "Mac arrow cursor";
@@ -591,17 +591,19 @@ void ColonyEngine::playAnimation() {
 				_gfx->computeScreenViewport();
 				needsDraw = true;
 			} else if (event.type == Common::EVENT_LBUTTONDOWN) {
-				int item = whichSprite(event.mouse);
+				int item = whichSprite(eventMouseToLogical(event.mouse));
 				if (item > 0) {
 					handleAnimationClick(item);
 					needsDraw = true;
 				}
 			} else if (event.type == Common::EVENT_RBUTTONDOWN) {
 				// DOS: right-click exits animation (AnimControl returns FALSE on button-up)
-				debugC(1, kColonyDebugAnimation, "Animation: RBUTTONDOWN exit at pos=%d,%d", event.mouse.x, event.mouse.y);
+				const Common::Point logical = eventMouseToLogical(event.mouse);
+				debugC(1, kColonyDebugAnimation, "Animation: RBUTTONDOWN exit at pos=%d,%d", logical.x, logical.y);
 				_animationRunning = false;
 			} else if (event.type == Common::EVENT_MOUSEMOVE) {
-				debugC(5, kColonyDebugAnimation, "Animation Mouse: %d, %d", event.mouse.x, event.mouse.y);
+				const Common::Point logical = eventMouseToLogical(event.mouse);
+				debugC(5, kColonyDebugAnimation, "Animation Mouse: %d, %d", logical.x, logical.y);
 			} else if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
 				if (event.customType == kActionEscape) {
 					openMainMenuDialog();
@@ -1779,8 +1781,11 @@ void ColonyEngine::moveObject(int index) {
 		linked.push_back(index);
 	}
 
-	// Get initial mouse position and animation origin
-	Common::Point old = _system->getEventManager()->getMousePos();
+	// Get initial mouse position and animation origin. getMousePos() returns
+	// virtual-screen coords; with kSupportsArbitraryResolutions that's
+	// window pixels, but sprite xloc/yloc and _screenR are in engine-logical
+	// coords. Convert so drag deltas are in the same units as the sprites.
+	Common::Point old = eventMouseToLogical(_system->getEventManager()->getMousePos());
 	int ox = _screenR.left + (_screenR.width() - 416) / 2;
 	ox = (ox / 8) * 8;
 	int oy = _screenR.top + (_screenR.height() - 264) / 2;
@@ -1857,7 +1862,7 @@ void ColonyEngine::moveObject(int index) {
 		if (!buttonDown)
 			break;
 
-		Common::Point cur = _system->getEventManager()->getMousePos();
+		Common::Point cur = eventMouseToLogical(_system->getEventManager()->getMousePos());
 		int dx = cur.x - old.x;
 		int dy = cur.y - old.y;
 
diff --git a/engines/colony/colony.cpp b/engines/colony/colony.cpp
index 0996a626682..dd764ff95d5 100644
--- a/engines/colony/colony.cpp
+++ b/engines/colony/colony.cpp
@@ -294,6 +294,26 @@ ColonyEngine::~ColonyEngine() {
 	delete _wm;
 }
 
+Common::Point ColonyEngine::eventMouseToLogical(const Common::Point &p) const {
+	const int sysW = _system->getWidth();
+	const int sysH = _system->getHeight();
+	if (sysW <= 0 || sysH <= 0 || (sysW == _width && sysH == _height))
+		return p;
+	return Common::Point((int)((int64)p.x * _width / sysW),
+		(int)((int64)p.y * _height / sysH));
+}
+
+void ColonyEngine::warpMouseLogical(int x, int y) {
+	const int sysW = _system->getWidth();
+	const int sysH = _system->getHeight();
+	if (sysW <= 0 || sysH <= 0 || (sysW == _width && sysH == _height)) {
+		_system->warpMouse(x, y);
+		return;
+	}
+	_system->warpMouse((int)((int64)x * sysW / _width),
+		(int)((int64)y * sysH / _height));
+}
+
 void ColonyEngine::pauseEngineIntern(bool pause) {
 	if (pause && _gfx && _level >= 1 && _level <= 7) {
 		if (_savedScreen) {
@@ -494,7 +514,7 @@ void ColonyEngine::updateMouseCapture(bool recenter) {
 
 	if (_mouseLocked && recenter) {
 		_mousePos = Common::Point(_centerX, _centerY);
-		_system->warpMouse(_centerX, _centerY);
+		warpMouseLogical(_centerX, _centerY);
 		_system->getEventManager()->purgeMouseEvents();
 	}
 }
@@ -880,10 +900,26 @@ Common::Error ColonyEngine::run() {
 
 		Common::Event event;
 		while (_system->getEventManager()->pollEvent(event)) {
-			// Let MacWindowManager handle menu events first
+			// Let MacWindowManager handle menu events first. The menu bar is
+			// drawn into _menuSurface (engine logical coords, _width×_height
+			// per colony.cpp:570), so its hit-testing rects are in logical
+			// space. With kSupportsArbitraryResolutions, event.mouse arrives
+			// in window pixels — pass a coord-scaled copy so the WM resolves
+			// menu clicks correctly. The original event is preserved for the
+			// engine's own handlers downstream.
 			if (_wm) {
 				bool wasMenuActive = _wm->isMenuActive();
-				if (_wm->processEvent(event)) {
+				Common::Event wmEvent = event;
+				if (event.type == Common::EVENT_MOUSEMOVE
+						|| event.type == Common::EVENT_LBUTTONDOWN
+						|| event.type == Common::EVENT_LBUTTONUP
+						|| event.type == Common::EVENT_RBUTTONDOWN
+						|| event.type == Common::EVENT_RBUTTONUP
+						|| event.type == Common::EVENT_MBUTTONDOWN
+						|| event.type == Common::EVENT_MBUTTONUP) {
+					wmEvent.mouse = eventMouseToLogical(event.mouse);
+				}
+				if (_wm->processEvent(wmEvent)) {
 					// WM consumed the event (menu interaction)
 					if (!wasMenuActive && _wm->isMenuActive()) {
 						_system->lockMouse(false);
@@ -1028,8 +1064,10 @@ Common::Error ColonyEngine::run() {
 			} else if (event.type == Common::EVENT_LBUTTONDOWN && (_mouseLocked || _cursorShoot)) {
 				cShoot();
 			} else if (event.type == Common::EVENT_MOUSEMOVE) {
-				_mousePos = event.mouse;
+				_mousePos = eventMouseToLogical(event.mouse);
 				if (_mouseLocked) {
+					// relMouse stays in window-pixel deltas regardless of
+					// resolution mode — keep raw for mouselook feel.
 					mouseDX += event.relMouse.x;
 					mouseDY += event.relMouse.y;
 					mouseMoved = true;
@@ -1049,7 +1087,7 @@ Common::Error ColonyEngine::run() {
 			}
 			// Warp back to center and purge remaining mouse events
 			// to prevent the warp from generating phantom deltas (Freescape pattern)
-			_system->warpMouse(_centerX, _centerY);
+			warpMouseLogical(_centerX, _centerY);
 			_system->getEventManager()->purgeMouseEvents();
 			mouseMoved = false;
 			mouseDX = mouseDY = 0;
diff --git a/engines/colony/colony.h b/engines/colony/colony.h
index a7f70bc0876..5e41698308a 100644
--- a/engines/colony/colony.h
+++ b/engines/colony/colony.h
@@ -663,6 +663,21 @@ private:
 	int occupiedObjectAt(int x, int y, const Locate *pobject);
 	void interactWithObject(int objNum);
 
+	// Convert a mouse coord delivered by the event manager into engine
+	// logical coords. With kSupportsArbitraryResolutions declared, the
+	// framework rewrites _currentState.gameWidth to the overlay (window)
+	// pixel size in recalculateDisplayAreas() — so g_system->getWidth()
+	// no longer matches our _width, and mouse events arrive in window
+	// pixels. The engine's hit-test math (whichSprite, _screenR) is in
+	// logical coords, so we have to scale back. Same pattern Freescape
+	// uses in mousePosToCrossairPos (freescape.cpp:593-597).
+	Common::Point eventMouseToLogical(const Common::Point &p) const;
+	// Inverse of eventMouseToLogical: warp the mouse to a position
+	// expressed in engine-logical coords. _system->warpMouse expects
+	// virtual-screen coords, which with kSupportsArbitraryResolutions
+	// is window pixels.
+	void warpMouseLogical(int x, int y);
+
 	// shoot.c: shooting and power management
 	void setPower(int p0, int p1, int p2);
 	void cShoot();
diff --git a/engines/colony/intro.cpp b/engines/colony/intro.cpp
index eee3ebe8dc5..6bbed3222f5 100644
--- a/engines/colony/intro.cpp
+++ b/engines/colony/intro.cpp
@@ -72,20 +72,40 @@ public:
 				if (processEvent(event))
 					continue;
 
+				// MacDialog button rects are in _screen coordinates (engine
+				// logical, e.g. 853×480). With kSupportsArbitraryResolutions
+				// the framework rewrites g_system->getWidth/Height to window
+				// pixel size, so event.mouse arrives in window pixels —
+				// convert back to _screen coords before hit-testing.
+				auto toLocal = [this](const Common::Point &p) -> Common::Point {
+					const int sysW = g_system->getWidth();
+					const int sysH = g_system->getHeight();
+					if (sysW <= 0 || sysH <= 0 || (sysW == _screen->w && sysH == _screen->h))
+						return p;
+					return Common::Point((int)((int64)p.x * _screen->w / sysW),
+						(int)((int64)p.y * _screen->h / sysH));
+				};
+
 				switch (event.type) {
 				case Common::EVENT_QUIT:
 					shouldQuitEngine = true;
 					shouldQuit = true;
 					break;
-				case Common::EVENT_MOUSEMOVE:
-					mouseMove(event.mouse.x, event.mouse.y);
+				case Common::EVENT_MOUSEMOVE: {
+					const Common::Point p = toLocal(event.mouse);
+					mouseMove(p.x, p.y);
 					break;
-				case Common::EVENT_LBUTTONDOWN:
-					mouseClick(event.mouse.x, event.mouse.y);
+				}
+				case Common::EVENT_LBUTTONDOWN: {
+					const Common::Point p = toLocal(event.mouse);
+					mouseClick(p.x, p.y);
 					break;
-				case Common::EVENT_LBUTTONUP:
-					shouldQuit = mouseRaise(event.mouse.x, event.mouse.y);
+				}
+				case Common::EVENT_LBUTTONUP: {
+					const Common::Point p = toLocal(event.mouse);
+					shouldQuit = mouseRaise(p.x, p.y);
 					break;
+				}
 				case Common::EVENT_KEYDOWN:
 					if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
 						_pressedButton = -1;
diff --git a/engines/colony/renderer_opengl.cpp b/engines/colony/renderer_opengl.cpp
index 6c1bf0cfd0b..7ced3b47798 100644
--- a/engines/colony/renderer_opengl.cpp
+++ b/engines/colony/renderer_opengl.cpp
@@ -99,6 +99,11 @@ public:
 
 private:
 	void useColor(uint32 color);
+	// Set glLineWidth scaled to window/logical ratio. Mirrors Freescape
+	// (gfx_opengl_shaders.cpp:692). Cached so back-to-back same-width
+	// line draws don't re-issue the GL call.
+	void applyLineWidthForLines();
+	float _lineWidth = 1.0f;
 	GLuint _overlayTexId = 0;
 
 	OSystem *_system = nullptr;
@@ -166,6 +171,17 @@ void OpenGLRenderer::useColor(uint32 color) {
 	}
 }
 
+void OpenGLRenderer::applyLineWidthForLines() {
+	const int sysW = _system ? _system->getWidth() : 0;
+	float w = 1.0f;
+	if (sysW > _width)
+		w = MAX(1.0f, (float)sysW / (float)_width);
+	if (w != _lineWidth) {
+		glLineWidth(w);
+		_lineWidth = w;
+	}
+}
+
 void OpenGLRenderer::clear(uint32 color) {
 	float r, g, b;
 	if (color & 0xFF000000) {
@@ -184,6 +200,7 @@ void OpenGLRenderer::clear(uint32 color) {
 
 void OpenGLRenderer::drawLine(int x1, int y1, int x2, int y2, uint32 color) {
 	useColor(color);
+	applyLineWidthForLines();
 	glBegin(GL_LINES);
 	glVertex2i(x1, y1);
 	glVertex2i(x2, y2);
@@ -192,6 +209,7 @@ void OpenGLRenderer::drawLine(int x1, int y1, int x2, int y2, uint32 color) {
 
 void OpenGLRenderer::drawRect(const Common::Rect &rect, uint32 color) {
 	useColor(color);
+	applyLineWidthForLines();
 	glBegin(GL_LINE_LOOP);
 	glVertex2i(rect.left, rect.top);
 	glVertex2i(rect.right, rect.top);
@@ -328,6 +346,7 @@ void OpenGLRenderer::draw3DWall(int x1, int y1, int x2, int y2, uint32 color) {
 
 		// Draw colored wireframe edges
 		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+		applyLineWidthForLines();
 		useColor(color);
 		glBegin(GL_QUADS);
 		glVertex3f(fx1, fy1, -160.0f);
@@ -572,6 +591,7 @@ void OpenGLRenderer::drawEllipse(int x, int y, int rx, int ry, uint32 color) {
 	glDisable(GL_DEPTH_TEST);
 
 	useColor(color);
+	applyLineWidthForLines();
 	glBegin(GL_LINE_LOOP);
 	for (int i = 0; i < 360; i += 10) {
 		float rad = i * M_PI / 180.0f;
@@ -636,9 +656,18 @@ void OpenGLRenderer::fillDitherRect(const Common::Rect &rect, uint32 color1, uin
 }
 
 void OpenGLRenderer::setPixel(int x, int y, uint32 color) {
+	// Draw as a 1×1 GL_QUADS instead of GL_POINTS. With kSupportsArbitrary
+	// Resolutions the viewport stretches logical coords to window pixels:
+	// a single GL_POINT renders as 1 framebuffer pixel (leaving gaps on
+	// HiDPI), while a 1×1 quad in logical coords gets scaled to cover the
+	// full logical-pixel cell — matching what the shader renderer does
+	// (renderer_opengl_shaders.cpp setPixel implementation).
 	useColor(color);
-	glBegin(GL_POINTS);
+	glBegin(GL_QUADS);
 	glVertex2i(x, y);
+	glVertex2i(x + 1, y);
+	glVertex2i(x + 1, y + 1);
+	glVertex2i(x, y + 1);
 	glEnd();
 }
 
@@ -652,6 +681,7 @@ void OpenGLRenderer::drawQuad(int x1, int y1, int x2, int y2, int x3, int y3, in
 	glEnd();
 	
 	glColor3ub(255, 255, 255);
+	applyLineWidthForLines();
 	glBegin(GL_LINE_LOOP);
 	glVertex2i(x1, y1);
 	glVertex2i(x2, y2);
@@ -669,8 +699,9 @@ void OpenGLRenderer::drawPolygon(const int *x, const int *y, int count, uint32 c
 		glVertex2i(x[i], y[i]);
 	}
 	glEnd();
-	
+
 	glColor3ub(255, 255, 255);
+	applyLineWidthForLines();
 	glBegin(GL_LINE_LOOP);
 	for (int i = 0; i < count; i++) {
 		glVertex2i(x[i], y[i]);
diff --git a/engines/colony/renderer_opengl_shaders.cpp b/engines/colony/renderer_opengl_shaders.cpp
index cf7ed31d02c..7ef33fb4d5c 100644
--- a/engines/colony/renderer_opengl_shaders.cpp
+++ b/engines/colony/renderer_opengl_shaders.cpp
@@ -97,6 +97,10 @@ private:
 	void uploadSolid(const float *positions, int vertCount);
 	void drawSolid(GLenum mode, const float *positions, int vertCount, const float rgba[4]);
 	void drawTexturedQuad(int x, int y, int w, int h);
+	// Set glLineWidth to scale with the window size (Freescape pattern,
+	// gfx_opengl_shaders.cpp:692). No-op when mode isn't a line primitive.
+	// Cached to avoid redundant state changes across same-width draws.
+	void applyLineWidth(GLenum mode);
 
 	void uploadSolid3D(const float *positions, int vertCount);
 	// allowStipple=true on the fill pass picks up _stippleActive; lines
@@ -134,6 +138,9 @@ private:
 	float _solidLastColor[4] = { -1.0f, -1.0f, -1.0f, -1.0f };
 	float _solid3dLastColor[4] = { -1.0f, -1.0f, -1.0f, -1.0f };
 
+	// Cached glLineWidth so applyLineWidth() can short-circuit repeats.
+	float _lineWidth = 1.0f;
+
 	bool _wireframe = true;
 	int64_t _wireframeFillColor = 0; // -1 = no fill, else color (palette idx or ARGB)
 
@@ -315,6 +322,24 @@ void OpenGLShaderRenderer::uploadSolid(const float *positions, int vertCount) {
 	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 2 * vertCount, positions, GL_DYNAMIC_DRAW);
 }
 
+void OpenGLShaderRenderer::applyLineWidth(GLenum mode) {
+	if (mode != GL_LINES && mode != GL_LINE_STRIP && mode != GL_LINE_LOOP)
+		return;
+	// Scale by window pixel width versus engine logical width — mirrors
+	// Freescape (gfx_opengl_shaders.cpp:692), MAX(1, ...) for safety.
+	// With kSupportsArbitraryResolutions, _system->getWidth() returns the
+	// overlay (window) pixel width; without it, getWidth() == _width and
+	// the ratio is 1.
+	const int sysW = _system ? _system->getWidth() : 0;
+	float w = 1.0f;
+	if (sysW > _width)
+		w = MAX(1.0f, (float)sysW / (float)_width);
+	if (w != _lineWidth) {
+		glLineWidth(w);
+		_lineWidth = w;
+	}
+}
+
 void OpenGLShaderRenderer::drawSolid(GLenum mode, const float *positions, int vertCount,
 		const float rgba[4]) {
 	if (vertCount <= 0)
@@ -331,6 +356,7 @@ void OpenGLShaderRenderer::drawSolid(GLenum mode, const float *positions, int ve
 		_solidLastColor[0] = rgba[0]; _solidLastColor[1] = rgba[1];
 		_solidLastColor[2] = rgba[2]; _solidLastColor[3] = rgba[3];
 	}
+	applyLineWidth(mode);
 	glDrawArrays(mode, 0, vertCount);
 }
 
@@ -807,6 +833,7 @@ void OpenGLShaderRenderer::drawSolid3D(GLenum mode, const float *positions, int
 			_solid3dLastColor[2] = rgba[2]; _solid3dLastColor[3] = rgba[3];
 		}
 	}
+	applyLineWidth(mode);
 	glDrawArrays(mode, 0, vertCount);
 }
 
diff --git a/engines/colony/savegame.cpp b/engines/colony/savegame.cpp
index 4dec96aa614..8d7df774b93 100644
--- a/engines/colony/savegame.cpp
+++ b/engines/colony/savegame.cpp
@@ -279,7 +279,12 @@ bool findInvalidActiveObjectSlot(const Common::Array<Thing> &objects, uint32 &in
 bool ColonyEngine::hasFeature(EngineFeature f) const {
 	return f == kSupportsReturnToLauncher ||
 		f == kSupportsLoadingDuringRuntime ||
-		f == kSupportsSavingDuringRuntime;
+		f == kSupportsSavingDuringRuntime ||
+		// Skip the OpenGL backend's FBO compositing pass: render directly
+		// to the default framebuffer (Freescape's pattern, freescape.cpp:
+		// 1170). Side effect: the user-facing screenshot hotkey now reads
+		// the correct buffer because there's no FBO bound between frames.
+		f == kSupportsArbitraryResolutions;
 }
 
 bool ColonyEngine::canSaveGameStateCurrently(Common::U32String *msg) {




More information about the Scummvm-git-logs mailing list