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

neuromancer noreply at scummvm.org
Sun Feb 8 20:33:30 UTC 2026


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

Summary:
192382fcf3 FREESCAPE: fixed colors reading CPC image in mode0
7ae92cf1d1 FREESCAPE: loading several images from castle amiga (demo)
b5aab81d02 FREESCAPE: music for castle amiga (demo)
f361b7c91d FREESCAPE: wb sound driver for dark amiga
c184ea0bb0 FREESCAPE: load additional images from castle cpc
6f27dba36d FREESCAPE: load additional images from castle amiga
d92aaeb1cd FREESCAPE: add sound for castle cpc
4f7d766b4a FREESCAPE: fixed a few bugs in the driller c64 music emulation
ba72f4991f FREESCAPE: add gate bitmap in castle cpc
49f2dc9b34 FREESCAPE: properly parse and show dark amiga title
b4868a69f7 FREESCAPE: EMSCRIPTEN -> USE_FORCED_GLES


Commit: 192382fcf35b7b02239a591e6f8d2f9e236d8890
    https://github.com/scummvm/scummvm/commit/192382fcf35b7b02239a591e6f8d2f9e236d8890
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:54+01:00

Commit Message:
FREESCAPE: fixed colors reading CPC image in mode0

Changed paths:
    engines/freescape/games/driller/cpc.cpp


diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index db9b895c1a3..404fc20da4f 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -80,9 +80,9 @@ byte getCPCPixelMode1(byte cpc_byte, int index) {
 byte getCPCPixelMode0(byte cpc_byte, int index) {
     if (index == 0) {
         // Extract Pixel 0 from the byte
-        return ((cpc_byte & 0x02) >> 1) |  // Bit 1 -> Bit 3 (MSB)
-               ((cpc_byte & 0x20) >> 4) |  // Bit 5 -> Bit 2
-               ((cpc_byte & 0x08) >> 1) |  // Bit 3 -> Bit 1
+        return ((cpc_byte & 0x02) << 2) |  // Bit 1 -> Bit 3 (MSB)
+               ((cpc_byte & 0x20) >> 3) |  // Bit 5 -> Bit 2
+               ((cpc_byte & 0x08) >> 2) |  // Bit 3 -> Bit 1
                ((cpc_byte & 0x80) >> 7);   // Bit 7 -> Bit 0 (LSB)
     }
     else if (index == 2) {


Commit: 7ae92cf1d12dd112ad92347e032eadbb24ebb26f
    https://github.com/scummvm/scummvm/commit/7ae92cf1d12dd112ad92347e032eadbb24ebb26f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:54+01:00

Commit Message:
FREESCAPE: loading several images from castle amiga (demo)

Changed paths:
    engines/freescape/games/castle/amiga.cpp
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/castle.h
    engines/freescape/gfx.h
    engines/freescape/gfx_opengl.cpp
    engines/freescape/gfx_opengl_shaders.cpp
    engines/freescape/gfx_tinygl.cpp


diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 7b941850e3e..61be4aeffbf 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -47,6 +47,25 @@ byte kAmigaCastlePalette[16][3] = {
 	{0xee, 0xee, 0xee},
 };
 
+byte kAmigaCastleRiddlePalette[16][3] = {
+	{0x00, 0x00, 0x00},
+	{0x44, 0x44, 0x44},
+	{0x66, 0x66, 0x66},
+	{0x88, 0x88, 0x88},
+	{0xaa, 0xaa, 0xaa},
+	{0xcc, 0x44, 0x00},
+	{0xee, 0xaa, 0x00},
+	{0x66, 0x22, 0x00},
+	{0x66, 0x22, 0x00},
+	{0x66, 0x22, 0x00},
+	{0x66, 0x22, 0x00},
+	{0x66, 0x22, 0x00},
+	{0xaa, 0x88, 0x00},
+	{0xaa, 0x66, 0x00},
+	{0x88, 0x44, 0x00},
+	{0xee, 0xcc, 0x66},
+};
+
 Graphics::ManagedSurface *CastleEngine::loadFrameFromPlanesVertical(Common::SeekableReadStream *file, int widthInBytes, int height) {
 	Graphics::ManagedSurface *surface;
 	surface = new Graphics::ManagedSurface();
@@ -79,6 +98,32 @@ Graphics::ManagedSurface *CastleEngine::loadFrameFromPlanesInternalVertical(Comm
 	return surface;
 }
 
+Graphics::ManagedSurface *CastleEngine::loadFrameFromPlanesInterleaved(Common::SeekableReadStream *file, int widthInWords, int height) {
+	int widthInPixels = widthInWords * 16;
+	Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+	surface->create(widthInPixels, height, Graphics::PixelFormat::createFormatCLUT8());
+	surface->fillRect(Common::Rect(0, 0, widthInPixels, height), 0);
+
+	for (int y = 0; y < height; y++) {
+		for (int col = 0; col < widthInWords; col++) {
+			uint16 planes[4];
+			for (int p = 0; p < 4; p++)
+				planes[p] = file->readUint16BE();
+
+			for (int bit = 0; bit < 16; bit++) {
+				int x = col * 16 + (15 - bit);
+				byte color = 0;
+				for (int p = 0; p < 4; p++) {
+					if (planes[p] & (1 << bit))
+						color |= (1 << p);
+				}
+				surface->setPixel(x, y, color);
+			}
+		}
+	}
+	return surface;
+}
+
 void CastleEngine::loadAssetsAmigaDemo() {
 	Common::File file;
 	file.open("x");
@@ -129,6 +174,47 @@ void CastleEngine::loadAssetsAmigaDemo() {
 	file.seek(0x2cf28 + 0x28 - 0x2 + 0x28);
 	_border = loadFrameFromPlanesVertical(&file, 160, 200);
 	_border->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+	// Menu background: 224x54 interleaved 4-plane (memory 0x36B9A, file 0x36BB6)
+	file.seek(0x36bb6);
+	_menu = loadFrameFromPlanesInterleaved(&file, 14, 54);
+	_menu->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+	file.seek(0x38952); // Spirit meter indicator background (memory 0x38936)
+	_spiritsMeterIndicatorBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 5, 10);
+	_spiritsMeterIndicatorBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+	file.seek(0x38ae2); // Spirit meter indicator (memory 0x38AC6)
+	_spiritsMeterIndicatorFrame = loadFrameFromPlanesInterleaved(&file, 1, 10);
+	_spiritsMeterIndicatorFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+	// Key sprites (memory 0x3C096, 12 frames, 16x7 each, interleaved 4-plane)
+	file.seek(0x3c0b2);
+	for (int i = 0; i < 12; i++) {
+		Graphics::ManagedSurface *frame = loadFrameFromPlanesInterleaved(&file, 1, 7);
+		frame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+		_keysBorderFrames.push_back(frame);
+	}
+
+	// Flag animation (memory 0x3C340, 5 frames, 32x11 each, interleaved 4-plane)
+	file.seek(0x3c35c);
+	for (int i = 0; i < 5; i++) {
+		Graphics::ManagedSurface *frame = loadFrameFromPlanesInterleaved(&file, 2, 11);
+		frame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+		_flagFrames.push_back(frame);
+	}
+
+	// Riddle frames (memory 0x3C6FA: top 20 rows + bg 1 row + bottom 8 rows, 256px wide)
+	file.seek(0x3c716);
+	_riddleTopFrame = loadFrameFromPlanesInterleaved(&file, 16, 20);
+	_riddleTopFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+
+	_riddleBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 16, 1);
+	_riddleBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+
+	_riddleBottomFrame = loadFrameFromPlanesInterleaved(&file, 16, 8);
+	_riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+
 	file.close();
 
 	_areaMap[2]->_groundColor = 1;
@@ -138,6 +224,32 @@ void CastleEngine::loadAssetsAmigaDemo() {
 
 void CastleEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 	drawStringInSurface(_currentArea->_name, 97, 182, 0, 0, surface);
+	uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
+
+	// Draw last collected key at (224, 164)
+	if (!_keysCollected.empty() && !_keysBorderFrames.empty()) {
+		int k = int(_keysCollected.size()) - 1;
+		if (k < int(_keysBorderFrames.size()))
+			surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_keysBorderFrames[k], 224, 164,
+				Common::Rect(0, 0, _keysBorderFrames[k]->w, _keysBorderFrames[k]->h), black);
+	}
+
+	// Draw flag animation at (288, 5)
+	if (!_flagFrames.empty()) {
+		int flagFrameIndex = (_ticks / 10) % _flagFrames.size();
+		surface->copyRectToSurface(*_flagFrames[flagFrameIndex], 288, 5,
+			Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));
+	}
+
+	// Draw spirit meter
+	if (_spiritsMeterIndicatorBackgroundFrame)
+		surface->copyRectToSurface((const Graphics::Surface)*_spiritsMeterIndicatorBackgroundFrame, 128, 160,
+			Common::Rect(0, 0, _spiritsMeterIndicatorBackgroundFrame->w, _spiritsMeterIndicatorBackgroundFrame->h));
+
+	if (_spiritsMeterIndicatorFrame) {
+		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_spiritsMeterIndicatorFrame, 128 + _spiritsMeterPosition, 160,
+			Common::Rect(0, 0, _spiritsMeterIndicatorFrame->w, _spiritsMeterIndicatorFrame->h), black);
+	}
 }
 
 } // End of namespace Freescape
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index deb2c661cc6..54b611a9c65 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -590,6 +590,33 @@ void CastleEngine::drawInfoMenu() {
 		for (int  i = 0; i < int(_keysCollected.size()) ; i++) {
 			int y = 58 + (i / 2) * 18;
 
+			if (i % 2 == 0) {
+				surface->copyRectToSurfaceWithKey(*_keysBorderFrames[i], 58, y, Common::Rect(0, 0, _keysBorderFrames[i]->w, _keysBorderFrames[i]->h), black);
+				keyRects.push_back(Common::Rect(58, y, 58 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
+			} else {
+				surface->copyRectToSurfaceWithKey(*_keysBorderFrames[i], 80, y, Common::Rect(0, 0, _keysBorderFrames[i]->w, _keysBorderFrames[i]->h), black);
+				keyRects.push_back(Common::Rect(80, y, 80 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
+			}
+		}
+	} else if (isAmiga() || isAtariST()) {
+		if (_menu)
+			surface->copyRectToSurface(*_menu, 47, 35, Common::Rect(0, 0, MIN<int>(_menu->w, surface->w - 47), MIN<int>(_menu->h, surface->h - 35)));
+
+		_gfx->readFromPalette(15, r, g, b);
+		front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+		drawStringInSurface(Common::String::format("%07d", score), 166, 71, front, black, surface);
+		drawStringInSurface(centerAndPadString(Common::String::format("%s", _messagesList[135 + shield / 6].c_str()), 10), 151, 102, front, black, surface);
+
+		Common::String keysCollected = _messagesList[141];
+		Common::replace(keysCollected, "X", Common::String::format("%d", _keysCollected.size()));
+		drawStringInSurface(keysCollected, 103, 41, front, black, surface);
+
+		Common::String spiritsDestroyedString = _messagesList[133];
+		Common::replace(spiritsDestroyedString, "X", Common::String::format("%d", spiritsDestroyed));
+		drawStringInSurface(spiritsDestroyedString, 145, 132, front, black, surface);
+
+		for (int i = 0; i < int(_keysCollected.size()); i++) {
+			int y = 58 + (i / 2) * 18;
 			if (i % 2 == 0) {
 				surface->copyRectToSurfaceWithKey(*_keysBorderFrames[i], 58, y, Common::Rect(0, 0, _keysBorderFrames[i]->w, _keysBorderFrames[i]->h), black);
 				keyRects.push_back(Common::Rect(58, y, 58 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
@@ -1122,6 +1149,8 @@ void CastleEngine::drawFullscreenRiddleAndWait(uint16 riddle) {
 	uint8 r, g, b;
 	_gfx->readFromPalette(frontColor, r, g, b);
 	uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+	if (isAmiga())
+		front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0xEE, 0xAA, 0x00);
 	uint32 transparent = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0x00, 0x00, 0x00);
 
 	Graphics::Surface *surface = new Graphics::Surface();
@@ -1180,6 +1209,10 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
 	} else if (isSpectrum() || isCPC()) {
 		x = 64;
 		y = 37;
+	} else if (isAmiga()) {
+		x = 32;
+		y = 33;
+		maxWidth = 139;
 	}
 	surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, x, y, Common::Rect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h));
 	for (y += _riddleTopFrame->h; y < maxWidth;) {
@@ -1198,6 +1231,9 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
 	} else if (isSpectrum() || isCPC()) {
 		x = 64;
 		y = 36;
+	} else if (isAmiga()) {
+		x = 40;
+		y = 32;
 	}
 
 	for (int i = 0; i < int(riddleMessages.size()); i++) {
@@ -1678,7 +1714,7 @@ void CastleEngine::drawBackground() {
 }
 
 void CastleEngine::updateThunder() {
-	if (!_thunderFrames[0])
+	if (_thunderFrames.empty() || !_thunderFrames[0])
 		return;
 
 	if (_thunderFrameDuration > 0) {
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index b106860e3ad..23952f23c36 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -108,6 +108,7 @@ public:
 
 	Graphics::ManagedSurface *loadFrameFromPlanesVertical(Common::SeekableReadStream *file, int widthInBytes, int height);
 	Graphics::ManagedSurface *loadFrameFromPlanesInternalVertical(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, int plane);
+	Graphics::ManagedSurface *loadFrameFromPlanesInterleaved(Common::SeekableReadStream *file, int widthInWords, int height);
 
 	Common::Array<Graphics::ManagedSurface *>_keysBorderFrames;
 	Common::Array<Graphics::ManagedSurface *>_keysMenuFrames;
diff --git a/engines/freescape/gfx.h b/engines/freescape/gfx.h
index 77895cb0258..a375eac9b27 100644
--- a/engines/freescape/gfx.h
+++ b/engines/freescape/gfx.h
@@ -193,6 +193,28 @@ public:
 		{ 0.4f, 2.0f }, //4
 	};
 
+	float _skyUvs672[16][2] = {
+		{ 0.0f, 0.0f }, //1
+		{ 0.0f, 2.0f }, //2
+		{ 0.6f, 2.0f }, //3
+		{ 0.6f, 0.0f }, //front //4
+
+		{ 0.0f, 2.0f }, //back //1
+		{ 0.6f, 2.0f }, //2
+		{ 0.6f, 0.0f }, //3
+		{ 0.0f, 0.0f }, //4
+
+		{ 0.0f, 0.0f }, //left //1
+		{ 0.6f, 0.0f }, //2
+		{ 0.6f, 2.0f }, //3
+		{ 0.0f, 2.0f }, //4
+
+		{ 0.6f, 0.0f }, //right //1
+		{ 0.0f, 0.0f }, //2
+		{ 0.0f, 2.0f }, //3
+		{ 0.6f, 2.0f }, //4
+	};
+
 	float _skyUvs128[16][2] = {
 		{ 0.0f, 0.0f }, //1
 		{ 0.0f, 2.0f }, //2
diff --git a/engines/freescape/gfx_opengl.cpp b/engines/freescape/gfx_opengl.cpp
index a73b4420ffc..e7c199a6172 100644
--- a/engines/freescape/gfx_opengl.cpp
+++ b/engines/freescape/gfx_opengl.cpp
@@ -171,6 +171,8 @@ void OpenGLRenderer::drawSkybox(Texture *texture, Math::Vector3d camera) {
 	glNormalPointer(GL_FLOAT, 0, _skyNormals);
 	if (texture->_width == 1008)
 		glTexCoordPointer(2, GL_FLOAT, 0, _skyUvs1008);
+	else if (texture->_width == 672)
+		glTexCoordPointer(2, GL_FLOAT, 0, _skyUvs672);
 	else if (texture->_width == 128)
 		glTexCoordPointer(2, GL_FLOAT, 0, _skyUvs128);
 	else
diff --git a/engines/freescape/gfx_opengl_shaders.cpp b/engines/freescape/gfx_opengl_shaders.cpp
index e4ea1a10b04..06ba3db09f1 100644
--- a/engines/freescape/gfx_opengl_shaders.cpp
+++ b/engines/freescape/gfx_opengl_shaders.cpp
@@ -182,6 +182,8 @@ void OpenGLShaderRenderer::drawSkybox(Texture *texture, Math::Vector3d camera) {
 	glBindBuffer(GL_ARRAY_BUFFER, _cubemapTexCoordVBO);
 	if (texture->_width == 1008)
 		glBufferData(GL_ARRAY_BUFFER, sizeof(_skyUvs1008), _skyUvs1008, GL_DYNAMIC_DRAW);
+	else if (texture->_width == 672)
+		glBufferData(GL_ARRAY_BUFFER, sizeof(_skyUvs672), _skyUvs672, GL_DYNAMIC_DRAW);
 	else if (texture->_width == 128)
 		glBufferData(GL_ARRAY_BUFFER, sizeof(_skyUvs128), _skyUvs128, GL_DYNAMIC_DRAW);
 	else
diff --git a/engines/freescape/gfx_tinygl.cpp b/engines/freescape/gfx_tinygl.cpp
index e5796397f68..b646b73c84a 100644
--- a/engines/freescape/gfx_tinygl.cpp
+++ b/engines/freescape/gfx_tinygl.cpp
@@ -115,6 +115,8 @@ void TinyGLRenderer::drawSkybox(Texture *texture, Math::Vector3d camera) {
 	tglNormalPointer(TGL_FLOAT, 0, _skyNormals);
 	if (texture->_width == 1008)
 		tglTexCoordPointer(2, TGL_FLOAT, 0, _skyUvs1008);
+	else if (texture->_width == 672)
+		tglTexCoordPointer(2, TGL_FLOAT, 0, _skyUvs672);
 	else if (texture->_width == 128)
 		tglTexCoordPointer(2, TGL_FLOAT, 0, _skyUvs128);
 	else


Commit: b5aab81d025981bc53a8903287e8adf67ce89607
    https://github.com/scummvm/scummvm/commit/b5aab81d025981bc53a8903287e8adf67ce89607
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:54+01:00

Commit Message:
FREESCAPE: music for castle amiga (demo)

Changed paths:
    engines/freescape/games/castle/amiga.cpp
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/castle.h


diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 61be4aeffbf..7588a98c7f1 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -22,6 +22,8 @@
 #include "common/file.h"
 #include "common/memstream.h"
 
+#include "audio/mods/protracker.h"
+
 #include "freescape/freescape.h"
 #include "freescape/games/castle/castle.h"
 #include "freescape/language/8bitDetokeniser.h"
@@ -215,6 +217,18 @@ void CastleEngine::loadAssetsAmigaDemo() {
 	_riddleBottomFrame = loadFrameFromPlanesInterleaved(&file, 16, 8);
 	_riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
 
+	// Load embedded ProTracker module for background music
+	// Module is at file offset 0x3D5A6 (memory 0x3D58A), ~86260 bytes
+	static const int kModOffset = 0x3D5A6;
+	file.seek(0, SEEK_END);
+	int fileSize = file.pos();
+	int modSize = fileSize - kModOffset;
+	if (modSize > 0) {
+		file.seek(kModOffset);
+		_modData.resize(modSize);
+		file.read(_modData.data(), modSize);
+	}
+
 	file.close();
 
 	_areaMap[2]->_groundColor = 1;
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 54b611a9c65..8fee1961a76 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -30,6 +30,8 @@
 #include "backends/keymapper/standard-actions.h"
 #include "common/translation.h"
 
+#include "audio/mods/protracker.h"
+
 #include "freescape/freescape.h"
 #include "freescape/games/castle/castle.h"
 #include "freescape/language/8bitDetokeniser.h"
@@ -361,6 +363,14 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
 			playSound(13, true, _soundFxHandle);
 		else
 			playSound(_soundIndexStart, false, _soundFxHandle);
+
+		// Start ProTracker background music for Amiga demo
+		if (isAmiga() && !_modData.empty() && !_mixer->isSoundHandleActive(_musicHandle)) {
+			Common::MemoryReadStream modStream(_modData.data(), _modData.size());
+			Audio::AudioStream *musicStream = Audio::makeProtrackerStream(&modStream);
+			if (musicStream)
+				_mixer->playStream(Audio::Mixer::kMusicSoundType, &_musicHandle, musicStream);
+		}
 	} else if (areaID == _endArea && entranceID == _endEntrance) {
 		_pitch = -85;
 	} else {
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 23952f23c36..815f605e5da 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -129,6 +129,7 @@ public:
 	Graphics::ManagedSurface *_endGameBackgroundFrame;
 	Graphics::ManagedSurface *_gameOverBackgroundFrame;
 
+	Common::Array<byte> _modData; // Embedded ProTracker module (Amiga demo)
 	Common::Array<int> _keysCollected;
 	bool _useRockTravel;
 	int _spiritsMeter;


Commit: f361b7c91dd93cccd07edb33dfa1536fd59bcfeb
    https://github.com/scummvm/scummvm/commit/f361b7c91dd93cccd07edb33dfa1536fd59bcfeb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:54+01:00

Commit Message:
FREESCAPE: wb sound driver for dark amiga

Changed paths:
  A engines/freescape/wb.cpp
  A engines/freescape/wb.h
    engines/freescape/games/dark/amiga.cpp
    engines/freescape/games/dark/dark.cpp
    engines/freescape/games/dark/dark.h
    engines/freescape/module.mk


diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 53468acdd33..9308d0c502c 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -42,6 +42,16 @@ void DarkEngine::loadAssetsAmigaFullGame() {
 	loadMessagesVariableSize(stream, 0x3d37, 66);
 	loadSoundsFx(stream, 0x34738 + 2, 11);
 
+	// Load HDSMUSIC.AM music data (Wally Beben custom engine)
+	// HDSMUSIC.AM is an embedded GEMDOS executable at stream offset $BA64
+	static const uint32 kHdsMusicOffset = 0xBA64;
+	static const uint32 kGemdosHeaderSize = 0x1C;
+	static const uint32 kHdsMusicTextSize = 0xF4BC;
+
+	stream->seek(kHdsMusicOffset + kGemdosHeaderSize);
+	_musicData.resize(kHdsMusicTextSize);
+	stream->read(_musicData.data(), kHdsMusicTextSize);
+
 	Common::Array<Graphics::ManagedSurface *> chars;
 	chars = getCharsAmigaAtariInternal(8, 8, - 7 - 8, 16, 16, stream, 0x1b0bc, 85);
 	_fontBig = Font(chars);
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index c4a71d2a0ce..42060f38873 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -28,6 +28,7 @@
 #include "freescape/games/dark/dark.h"
 #include "freescape/language/8bitDetokeniser.h"
 #include "freescape/objects/global.h"
+#include "freescape/wb.h"
 #include "freescape/objects/connections.h"
 
 namespace Freescape {
@@ -293,8 +294,16 @@ void DarkEngine::initGameState() {
 	_angleRotationIndex = 0;
 	_playerStepIndex = 6;
 
-	// Start playing music, if any, in any supported format
-	playMusic("Dark Side Theme");
+	// Start background music
+	if (isAmiga() && !_musicData.empty()) {
+		Audio::AudioStream *musicStream = makeWallyBebenStream(
+			_musicData.data(), _musicData.size(), 1);
+		if (musicStream) {
+			_mixer->stopHandle(_musicHandle);
+			_mixer->playStream(Audio::Mixer::kMusicSoundType,
+				&_musicHandle, musicStream);
+		}
+	}
 }
 
 void DarkEngine::loadAssets() {
diff --git a/engines/freescape/games/dark/dark.h b/engines/freescape/games/dark/dark.h
index c813fab4aa6..b2af7cd7923 100644
--- a/engines/freescape/games/dark/dark.h
+++ b/engines/freescape/games/dark/dark.h
@@ -20,6 +20,7 @@
  */
 
 #include "audio/mixer.h"
+#include "common/array.h"
 
 namespace Freescape {
 
@@ -104,6 +105,8 @@ public:
 	int _soundIndexDestroyECD;
 	Audio::SoundHandle _soundFxHandleJetpack;
 
+	Common::Array<byte> _musicData; // HDSMUSIC.AM TEXT segment (Amiga)
+
 	void drawString(const DarkFontSize size, const Common::String &str, int x, int y, uint32 primaryColor, uint32 secondaryColor, uint32 backColor, Graphics::Surface *surface);
 	void drawInfoMenu() override;
 
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 1f607cf9e57..8108c47c156 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -50,7 +50,8 @@ MODULE_OBJS := \
 	sweepAABB.o \
 	sound.o \
 	ui.o \
-	unpack.o
+	unpack.o \
+	wb.o
 
 ifdef USE_TINYGL
 MODULE_OBJS += \
diff --git a/engines/freescape/wb.cpp b/engines/freescape/wb.cpp
new file mode 100644
index 00000000000..e64e90cc922
--- /dev/null
+++ b/engines/freescape/wb.cpp
@@ -0,0 +1,934 @@
+/* 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/>.
+ *
+ */
+
+/**
+ * Wally Beben custom music engine player.
+ *
+ * Used in the Amiga version of Dark Side (Incentive Software, 1988).
+ * HDSMUSIC.AM is an embedded GEMDOS executable containing a 4-channel
+ * music engine with its own byte-stream pattern format, ~57KB of PCM
+ * samples, and ADSR volume envelopes.
+ *
+ * Assembly reference addresses (TEXT-relative, HDSMUSIC.AM):
+ *   $0004  Tick entry point (called at 50 Hz from VBI)
+ *   $0012  Command mailbox: 0=stop, 1/2=play song, $FF=playing
+ *   $010C  Main sequencer body (readPatternCommands equivalent)
+ *   $0268  Pattern command dispatcher ($E0=inst, $C0=env, $80=dur, etc.)
+ *   $030C  Note-on handler (triggerNote equivalent)
+ *   $068C  Envelope processing (processEnvelope equivalent)
+ *   $0AAE  Period table (48 x uint16 BE)
+ *   $0C42  Sample pointer table (16 x uint32 BE, TEXT-relative)
+ *   $0C82  Instrument table (16 x 8 bytes)
+ *   $0D02  Arpeggio interval lookup (8 bytes)
+ *   $0D0A  Envelope table (10 x 8 bytes)
+ *   $0DBA  Song table (2 songs x 4 channel pointers)
+ *   $0DCA  Pattern pointer table (up to ~71 x uint32 BE)
+ *   $14B3  PCM sample data start (~57KB through $F4BB)
+ *
+ */
+
+#include "freescape/wb.h"
+#include "freescape/freescape.h"
+#include "audio/mods/paula.h"
+#include "common/endian.h"
+#include "common/debug.h"
+#include "common/util.h"
+
+namespace Freescape {
+
+// TEXT-relative offsets for data tables within HDSMUSIC.AM
+// All addresses verified against disassembly of the 68000 code.
+static const uint32 kPeriodTableOffset    = 0x0AAE; // 48 x uint16 BE (note 0=silence, 1-47=C-1..B-3)
+static const uint32 kSamplePtrTableOffset = 0x0C42; // 16 x uint32 BE (TEXT-relative PCM offsets)
+static const uint32 kInstrumentTableOffset = 0x0C82; // 16 x 8 bytes (sample#, loopFlag, len, loopOff, loopLen)
+static const uint32 kArpeggioIntervalsOffset = 0x0D02; // 8 bytes (semitone offsets for arpeggio bitmask)
+static const uint32 kEnvelopeTableOffset  = 0x0D0A; // 10 x 8 bytes (atk, dec, fadeRate, rel, mod, vib, arp, flags)
+static const uint32 kSongTableOffset      = 0x0DBA; // 2 songs x 4 channels x uint32 BE order-list pointers
+static const uint32 kPatternPtrTableOffset = 0x0DCA; // up to 128 x uint32 BE (overlaps Song 2 order ptrs)
+static const uint32 kMaxPatternEntries    = 128;
+
+class WallyBebenStream : public Audio::Paula {
+public:
+	WallyBebenStream(const byte *data, uint32 dataSize, int songNum, int rate, bool stereo);
+	~WallyBebenStream() override;
+
+private:
+	void interrupt() override;
+
+	// --- Data tables (parsed from HDSMUSIC.AM TEXT segment) ---
+
+	const byte *_data;
+	uint32 _dataSize;
+
+	uint16 _periods[48];
+
+	struct InstrumentDesc {
+		byte sampleIndex;
+		byte loopFlag;
+		uint16 totalLength;
+		uint16 loopOffset;
+		uint16 loopLength;
+	};
+	InstrumentDesc _instruments[16];
+
+	struct EnvelopeDesc {
+		byte attackLevel;
+		byte decayTarget;
+		byte sustainLevel;
+		byte releaseRate;
+		byte modDepth;
+		byte vibratoWave;
+		byte arpeggioMask;
+		byte flags;
+	};
+	EnvelopeDesc _envelopes[10];
+
+	// Sample offsets within the data buffer
+	uint32 _sampleOffsets[16];
+
+	// Song order list pointers (TEXT-relative)
+	uint32 _songOrderPtrs[2][4];
+
+	// Pattern pointer table (TEXT+$DCA, up to 128 entries)
+	uint32 _patternPtrs[kMaxPatternEntries];
+	uint32 _numPatterns; // actual number of valid entries loaded
+	uint32 _firstSampleOffset; // lowest sample data offset — upper bound for pattern pointers
+
+	// Arpeggio interval lookup
+	byte _arpeggioIntervals[8];
+
+	// --- Per-channel state ---
+
+	struct ChannelState {
+		// Order list
+		uint32 orderListOffset; // TEXT-relative offset to order list
+		int orderListPos;       // Current byte position in order list
+		int8 transpose;         // Semitone transpose from order list
+
+		// Current pattern
+		uint32 patternOffset;   // TEXT-relative offset to current pattern
+		int patternPos;         // Current byte position in pattern
+
+		// Note state
+		byte note;              // Current note (0=rest, 1-47)
+		byte prevNote;          // Previous note (for portamento)
+		byte duration;          // Note duration in sequencer steps
+		int durationCounter;    // Remaining steps for current note
+
+		// Instrument / envelope selection
+		byte instrumentIdx;     // 0-15
+		byte envelopeIdx;       // 0-9
+
+		// Volume envelope
+		byte volume;            // Current output volume (0-64)
+		byte attackLevel;
+		byte decayTarget;
+		byte sustainLevel;
+		byte releaseRate;
+		byte envelopePhase;     // 0=attack, 1=decay, 2=sustain, 3=release
+		byte modDepth;
+
+		// Effects
+		byte effectMode;        // 0=none, 1=porta/arpeggio, 2=envelope vibrato
+		bool portaUp;
+		bool portaDown;
+		int16 portaStep;
+		int16 portaTarget;
+		byte arpeggioMask;
+		byte arpeggioPos;
+		byte vibratoPos;
+
+		// Period
+		int16 basePeriod;       // From note lookup
+		int16 outputPeriod;     // After effects
+
+		// Delay
+		byte delay;
+		byte delayCounter;
+
+		bool active;
+	};
+	ChannelState _channels[4];
+
+	// --- Global state ---
+
+	bool _musicActive;
+	byte _tickSpeed;    // Ticks between sequencer steps
+	byte _tickCounter;  // Current tick count (counts up to _tickSpeed)
+
+	// Arpeggio working table (built by buildArpeggioTable)
+	byte _arpeggioTable[16];
+	int _arpeggioTableLen;
+
+	// --- Methods ---
+
+	void loadTables();
+	void startSong(int songNum);
+	void initChannel(int ch);
+	void readOrderList(int ch);
+	void readPatternCommands(int ch);
+	void triggerNote(int ch);
+	void processEffects(int ch);
+	void processEnvelope(int ch);
+	void buildArpeggioTable(byte mask);
+	uint16 getPeriod(int note) const;
+
+	byte readDataByte(uint32 offset) const {
+		if (offset < _dataSize)
+			return _data[offset];
+		return 0;
+	}
+
+	uint16 readDataWord(uint32 offset) const {
+		if (offset + 1 < _dataSize)
+			return READ_BE_UINT16(_data + offset);
+		return 0;
+	}
+
+	uint32 readDataLong(uint32 offset) const {
+		if (offset + 3 < _dataSize)
+			return READ_BE_UINT32(_data + offset);
+		return 0;
+	}
+};
+
+// ---------------------------------------------------------------------------
+// Construction / data loading
+// ---------------------------------------------------------------------------
+
+WallyBebenStream::WallyBebenStream(const byte *data, uint32 dataSize,
+                                   int songNum, int rate, bool stereo)
+	: Paula(stereo, rate, rate / 50), // 50 Hz PAL VBI interrupt rate
+	  _data(data), _dataSize(dataSize),
+	  _musicActive(false), _tickSpeed(6), _tickCounter(0),
+	  _arpeggioTableLen(0), _numPatterns(0), _firstSampleOffset(0) {
+
+	memset(_periods, 0, sizeof(_periods));
+	memset(_instruments, 0, sizeof(_instruments));
+	memset(_envelopes, 0, sizeof(_envelopes));
+	memset(_sampleOffsets, 0, sizeof(_sampleOffsets));
+	memset(_songOrderPtrs, 0, sizeof(_songOrderPtrs));
+	memset(_patternPtrs, 0, sizeof(_patternPtrs));
+	memset(_arpeggioIntervals, 0, sizeof(_arpeggioIntervals));
+	memset(_channels, 0, sizeof(_channels));
+	memset(_arpeggioTable, 0, sizeof(_arpeggioTable));
+
+	// Standard Amiga panning: channels 0,3 left — channels 1,2 right
+	setChannelPanning(0, PANNING_LEFT);
+	setChannelPanning(1, PANNING_RIGHT);
+	setChannelPanning(2, PANNING_RIGHT);
+	setChannelPanning(3, PANNING_LEFT);
+
+	loadTables();
+	startSong(songNum);
+}
+
+WallyBebenStream::~WallyBebenStream() {
+}
+
+void WallyBebenStream::loadTables() {
+	// Period table: 48 x uint16 BE at TEXT+$AAE
+	for (int i = 0; i < 48; i++) {
+		_periods[i] = readDataWord(kPeriodTableOffset + i * 2);
+	}
+
+	// Sample pointer table: 16 x uint32 BE at TEXT+$C42
+	// These are TEXT-relative offsets to PCM sample data
+	for (int i = 0; i < 16; i++) {
+		_sampleOffsets[i] = readDataLong(kSamplePtrTableOffset + i * 4);
+	}
+
+	// Instrument table: 16 x 8 bytes at TEXT+$C82
+	for (int i = 0; i < 16; i++) {
+		uint32 off = kInstrumentTableOffset + i * 8;
+		_instruments[i].sampleIndex  = readDataByte(off + 0);
+		_instruments[i].loopFlag     = readDataByte(off + 1);
+		_instruments[i].totalLength  = readDataWord(off + 2);
+		_instruments[i].loopOffset   = readDataWord(off + 4);
+		_instruments[i].loopLength   = readDataWord(off + 6);
+	}
+
+	// Arpeggio interval table: 8 bytes at TEXT+$D02
+	for (int i = 0; i < 8; i++) {
+		_arpeggioIntervals[i] = readDataByte(kArpeggioIntervalsOffset + i);
+	}
+
+	// Envelope table: 10 x 8 bytes at TEXT+$D0A
+	for (int i = 0; i < 10; i++) {
+		uint32 off = kEnvelopeTableOffset + i * 8;
+		_envelopes[i].attackLevel  = readDataByte(off + 0);
+		_envelopes[i].decayTarget  = readDataByte(off + 1);
+		_envelopes[i].sustainLevel = readDataByte(off + 2);
+		_envelopes[i].releaseRate  = readDataByte(off + 3);
+		_envelopes[i].modDepth     = readDataByte(off + 4);
+		_envelopes[i].vibratoWave  = readDataByte(off + 5);
+		_envelopes[i].arpeggioMask = readDataByte(off + 6);
+		_envelopes[i].flags        = readDataByte(off + 7);
+	}
+
+	// Song table: 2 songs x 4 channels x uint32 BE at TEXT+$DBA
+	for (int s = 0; s < 2; s++) {
+		for (int ch = 0; ch < 4; ch++) {
+			_songOrderPtrs[s][ch] = readDataLong(kSongTableOffset + s * 16 + ch * 4);
+		}
+	}
+
+	// Compute the first sample offset — this is the upper bound for valid
+	// pattern/order-list data. Anything at or above this is PCM sample data.
+	_firstSampleOffset = _dataSize;
+	for (int i = 0; i < 16; i++) {
+		if (_sampleOffsets[i] > 0 && _sampleOffsets[i] < _firstSampleOffset)
+			_firstSampleOffset = _sampleOffsets[i];
+	}
+
+	// Pattern pointer table at TEXT+$DCA.
+	// Valid pattern pointers must be > 0 and < _firstSampleOffset (i.e., they
+	// point into the song data area between the tables and the PCM samples).
+	// Entries at or beyond _firstSampleOffset are stale data, not real patterns.
+	_numPatterns = 0;
+	for (uint32 i = 0; i < kMaxPatternEntries; i++) {
+		uint32 ptr = readDataLong(kPatternPtrTableOffset + i * 4);
+		_patternPtrs[i] = ptr;
+		if (ptr > 0 && ptr < _firstSampleOffset)
+			_numPatterns = i + 1;
+	}
+
+	// Debug: dump loaded data tables for verification
+	debug(3, "WB: Data loaded from HDSMUSIC.AM TEXT segment (%u bytes)", _dataSize);
+
+	debug(3, "WB: Period table (first 12): %d %d %d %d %d %d %d %d %d %d %d %d",
+		_periods[0], _periods[1], _periods[2], _periods[3],
+		_periods[4], _periods[5], _periods[6], _periods[7],
+		_periods[8], _periods[9], _periods[10], _periods[11]);
+
+	for (int i = 0; i < 16; i++) {
+		if (_sampleOffsets[i])
+			debug(3, "WB: Sample %d: offset=$%X", i, _sampleOffsets[i]);
+	}
+
+	for (int i = 0; i < 16; i++) {
+		const InstrumentDesc &inst = _instruments[i];
+		if (inst.totalLength > 0)
+			debug(3, "WB: Inst %d: sample=%d loop=%d len=%d loopOff=%d loopLen=%d",
+				i, inst.sampleIndex, inst.loopFlag, inst.totalLength,
+				inst.loopOffset, inst.loopLength);
+	}
+
+	for (int i = 0; i < 10; i++) {
+		const EnvelopeDesc &env = _envelopes[i];
+		debug(3, "WB: Env %d: atk=%d dec=%d sus=%d rel=%d mod=%d arp=$%02X",
+			i, env.attackLevel, env.decayTarget, env.sustainLevel,
+			env.releaseRate, env.modDepth, env.arpeggioMask);
+	}
+
+	debug(3, "WB: Song 1 order ptrs: $%X $%X $%X $%X",
+		_songOrderPtrs[0][0], _songOrderPtrs[0][1],
+		_songOrderPtrs[0][2], _songOrderPtrs[0][3]);
+	debug(3, "WB: Song 2 order ptrs: $%X $%X $%X $%X",
+		_songOrderPtrs[1][0], _songOrderPtrs[1][1],
+		_songOrderPtrs[1][2], _songOrderPtrs[1][3]);
+
+	debug(3, "WB: %d valid patterns (firstSampleOffset=$%X)", _numPatterns, _firstSampleOffset);
+	for (uint32 i = 0; i < _numPatterns; i++) {
+		if (_patternPtrs[i])
+			debug(3, "WB: Pattern %d: offset=$%X", i, _patternPtrs[i]);
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Song init
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::startSong(int songNum) {
+	_musicActive = false;
+
+	if (songNum < 1 || songNum > 2)
+		return;
+
+	int songIdx = songNum - 1;
+	_tickSpeed = 6;
+	_tickCounter = 0;
+	_arpeggioTableLen = 0;
+
+	// Silence all Paula channels
+	for (int ch = 0; ch < NUM_VOICES; ch++) {
+		clearVoice(ch);
+	}
+
+	// Initialize each channel from the song table
+	for (int ch = 0; ch < 4; ch++) {
+		initChannel(ch);
+		_channels[ch].orderListOffset = _songOrderPtrs[songIdx][ch];
+		_channels[ch].orderListPos = 0;
+		_channels[ch].active = true;
+
+		// Read first order list entry to get the initial pattern
+		readOrderList(ch);
+	}
+
+	_musicActive = true;
+	startPaula();
+
+	debug(3, "WB: Song %d started, tickSpeed=%d", songNum, _tickSpeed);
+	for (int ch = 0; ch < 4; ch++) {
+		debug(3, "WB: ch%d orderList=$%X pattern=$%X",
+			ch, _channels[ch].orderListOffset, _channels[ch].patternOffset);
+	}
+}
+
+void WallyBebenStream::initChannel(int ch) {
+	ChannelState &c = _channels[ch];
+	memset(&c, 0, sizeof(ChannelState));
+	c.duration = 1;
+	c.durationCounter = 0; // Will trigger readPatternCommands on first tick
+	c.envelopePhase = 3;   // Start in release (silent) until note-on
+
+	// Default envelope params: full volume sustain, so notes before any
+	// $C0 envelope command still produce sound (Env 0 has all zeros = silence)
+	c.attackLevel = 64;
+	c.decayTarget = 64;
+	c.sustainLevel = 0; // Instant transition (no fade)
+	c.releaseRate = 0;
+}
+
+// ---------------------------------------------------------------------------
+// Order list reader — advances to the next pattern for a channel
+// Asm ref: TEXT+$01A0 (order list processing)
+// Order list format: $00-$C0=pattern#, $C1-$FE=transpose, $FF=loop
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::readOrderList(int ch) {
+	ChannelState &c = _channels[ch];
+
+	for (int safety = 0; safety < 256; safety++) {
+		if (c.orderListOffset + c.orderListPos >= _dataSize)
+			break;
+
+		byte cmd = readDataByte(c.orderListOffset + c.orderListPos);
+		c.orderListPos++;
+
+		if (cmd == 0xFF) {
+			// Loop song: reset order list to beginning
+			c.orderListPos = 0;
+			continue;
+		}
+
+		if (cmd > 0xC0) {
+			// Transpose command: transpose = (cmd + 0x20) & 0xFF
+			// Stored as signed offset
+			c.transpose = (int8)((cmd + 0x20) & 0xFF);
+			continue;
+		}
+
+		// Pattern index (0 to $C0) — look up in pattern pointer table
+		// Validate: must be within table AND point to real pattern data (< sample area)
+		if (cmd < kMaxPatternEntries && _patternPtrs[cmd] > 0 && _patternPtrs[cmd] < _firstSampleOffset) {
+			c.patternOffset = _patternPtrs[cmd];
+			c.patternPos = 0;
+			debugC(3, kFreescapeDebugParser, "WB: ch%d order -> pattern %d (offset $%04X)", ch, cmd, c.patternOffset);
+		} else {
+			warning("WallyBeben: ch%d pattern index %d invalid (ptr=$%X, sampleStart=$%X)",
+				ch, cmd, (cmd < kMaxPatternEntries) ? _patternPtrs[cmd] : 0, _firstSampleOffset);
+			c.patternOffset = _patternPtrs[0];
+			c.patternPos = 0;
+		}
+		return;
+	}
+
+	warning("WallyBeben: ch%d order list safety limit hit", ch);
+}
+
+// ---------------------------------------------------------------------------
+// Pattern command reader — reads byte stream until a note is found
+// Asm ref: TEXT+$0268 (command dispatcher)
+// Pattern format: $FF=end-pattern, $FE=end-song, $F0-$FD=speed,
+//   $E0-$EF=instrument, $C0-$DF=envelope, $80-$BF=duration,
+//   $7F/$7E=portamento, $7D/$7C=vibrato, $7B=arpeggio,
+//   $7A=delay, $00-$79=note (0=rest, 1-47=C-1..B-3)
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::readPatternCommands(int ch) {
+	ChannelState &c = _channels[ch];
+
+	for (int safety = 0; safety < 256; safety++) {
+		if (c.patternOffset + c.patternPos >= _dataSize)
+			break;
+
+		byte cmd = readDataByte(c.patternOffset + c.patternPos);
+		c.patternPos++;
+
+		if (cmd == 0xFF) {
+			// End of pattern — advance order list
+			readOrderList(ch);
+			continue;
+		}
+
+		if (cmd == 0xFE) {
+			// End of song
+			_musicActive = false;
+			return;
+		}
+
+		if (cmd == 0xDD) {
+			// Song change — read parameter byte, ignore for now
+			c.patternPos++;
+			continue;
+		}
+
+		if (cmd >= 0xF0) {
+			// Set speed: low nibble
+			_tickSpeed = cmd & 0x0F;
+			if (_tickSpeed == 0)
+				_tickSpeed = 1;
+			continue;
+		}
+
+		if (cmd >= 0xE0) {
+			// Set instrument: low nibble (0-15)
+			c.instrumentIdx = cmd & 0x0F;
+			continue;
+		}
+
+		if (cmd >= 0xC0) {
+			// Set envelope: low 5 bits (0-31, but only 1-9 valid)
+			// Asm ref: TEXT+$2C8 — envelope command handler
+			// $C0 (index 0) is a no-op: Env 0 is all zeros (sentinel entry).
+			// The original engine treats index 0 as "no envelope change".
+			byte envIdx = cmd & 0x1F;
+			if (envIdx == 0 || envIdx > 9) {
+				// Index 0 or out-of-range: skip, keep current envelope params
+				continue;
+			}
+
+			c.envelopeIdx = envIdx;
+
+			// Copy envelope parameters into channel state immediately
+			// (original engine loads params on $C0 command, not on note-on)
+			const EnvelopeDesc &env = _envelopes[c.envelopeIdx];
+			c.attackLevel  = MIN(env.attackLevel,  (byte)64);
+			c.decayTarget  = MIN(env.decayTarget,  (byte)64);
+			c.sustainLevel = MIN(env.sustainLevel, (byte)64);
+			c.releaseRate  = env.releaseRate;
+			c.modDepth     = env.modDepth;
+
+			// Envelope-triggered arpeggio/vibrato from envelope table
+			if (env.arpeggioMask != 0) {
+				c.effectMode = 2;
+				c.arpeggioMask = env.arpeggioMask;
+				buildArpeggioTable(env.arpeggioMask);
+			}
+			continue;
+		}
+
+		if (cmd >= 0x80) {
+			// Set duration: low 6 bits (0-63)
+			c.duration = cmd & 0x3F;
+			if (c.duration == 0)
+				c.duration = 1;
+			continue;
+		}
+
+		if (cmd == 0x7F) {
+			// Portamento up
+			c.portaUp = true;
+			c.portaDown = false;
+			c.effectMode = 1;
+			continue;
+		}
+
+		if (cmd == 0x7E) {
+			// Portamento down
+			c.portaDown = true;
+			c.portaUp = false;
+			c.effectMode = 1;
+			continue;
+		}
+
+		if (cmd == 0x7D) {
+			// Vibrato type 1
+			byte param = readDataByte(c.patternOffset + c.patternPos);
+			c.patternPos++;
+			c.effectMode = 1;
+			c.arpeggioMask = param;
+			buildArpeggioTable(param);
+			continue;
+		}
+
+		if (cmd == 0x7C) {
+			// Vibrato type 2
+			byte param = readDataByte(c.patternOffset + c.patternPos);
+			c.patternPos++;
+			c.effectMode = 2;
+			c.arpeggioMask = param;
+			buildArpeggioTable(param);
+			continue;
+		}
+
+		if (cmd == 0x7B) {
+			// Arpeggio
+			byte base = readDataByte(c.patternOffset + c.patternPos);
+			c.patternPos++;
+			byte rate = readDataByte(c.patternOffset + c.patternPos);
+			c.patternPos++;
+			c.effectMode = 1;
+			c.arpeggioMask = 0;
+			c.arpeggioPos = 0;
+			// Store arpeggio base note with transpose applied
+			_arpeggioTable[0] = base;
+			_arpeggioTable[1] = rate;
+			_arpeggioTableLen = 2;
+			continue;
+		}
+
+		if (cmd == 0x7A) {
+			// Set delay
+			byte param = readDataByte(c.patternOffset + c.patternPos);
+			c.patternPos++;
+			c.delay = param;
+			continue;
+		}
+
+		// Note value (0x00-0x79)
+		c.prevNote = c.note;
+		c.note = cmd;
+		c.durationCounter = c.duration;
+		triggerNote(ch);
+		return; // One note per sequencer step
+	}
+
+	warning("WallyBeben: ch%d pattern read safety limit hit", ch);
+}
+
+// ---------------------------------------------------------------------------
+// Note trigger — load instrument, set up Paula channel, reset envelope
+// Asm ref: TEXT+$030C (note-on handler)
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::triggerNote(int ch) {
+	ChannelState &c = _channels[ch];
+
+	if (c.note == 0) {
+		// Rest — silence channel
+		c.outputPeriod = 0;
+		c.volume = 0;
+		c.envelopePhase = 3;
+		setChannelVolume(ch, 0);
+		return;
+	}
+
+	// Apply transpose and clamp
+	int note = c.note + c.transpose;
+	if (note < 1) note = 1;
+	if (note > 47) note = 47;
+
+	// Look up period
+	c.basePeriod = getPeriod(note);
+	c.outputPeriod = c.basePeriod;
+
+	if (c.basePeriod == 0) {
+		warning("WallyBeben: ch%d note %d (raw %d + transpose %d) has period 0",
+			ch, note, c.note, c.transpose);
+		return;
+	}
+
+	// Load instrument
+	const InstrumentDesc &inst = _instruments[c.instrumentIdx];
+	byte sampleIdx = inst.sampleIndex;
+	if (sampleIdx >= 16)
+		sampleIdx = 0;
+
+	uint32 sampleOffset = _sampleOffsets[sampleIdx];
+	if (sampleOffset >= _dataSize) {
+		warning("WallyBeben: ch%d sample %d offset $%X out of range (dataSize=$%X)",
+			ch, sampleIdx, sampleOffset, _dataSize);
+		return;
+	}
+
+	const int8 *sampleData = (const int8 *)(_data + sampleOffset);
+	uint32 maxLen = _dataSize - sampleOffset;
+
+	// Instrument lengths are in bytes
+	uint32 totalLen = MIN((uint32)inst.totalLength, maxLen);
+	uint32 loopOff = MIN((uint32)inst.loopOffset, totalLen);
+	uint32 loopLen = inst.loopLength;
+
+	debug(5, "WB: ch%d note=%d period=%d inst=%d sample=%d offset=$%X len=%d loop=%d/%d",
+		ch, note, c.basePeriod, c.instrumentIdx, sampleIdx, sampleOffset,
+		totalLen, loopOff, loopLen);
+
+	if (totalLen < 4) {
+		warning("WallyBeben: ch%d sample too short (%d bytes)", ch, totalLen);
+		return;
+	}
+
+	// setChannelData calls enableChannel internally — do NOT call enableChannel again!
+	if (inst.loopFlag && loopLen > 2 && loopOff + loopLen <= totalLen) {
+		// Looped sample: initial play of full sample, then loop region
+		setChannelData(ch,
+			sampleData,                // initial data pointer
+			sampleData + loopOff,      // loop start pointer
+			totalLen,                  // initial length in bytes
+			loopLen);                  // loop length in bytes
+	} else {
+		// One-shot: play once, then 1-byte silence loop
+		setChannelData(ch,
+			sampleData,
+			sampleData + totalLen - 2,
+			totalLen,                  // length in bytes
+			1);
+	}
+
+	setChannelPeriod(ch, c.outputPeriod);
+
+	// Reset envelope phase — params were already loaded by $C0 command
+	// (or default to full volume from initChannel)
+	// Asm ref: TEXT+$30C — note-on envelope reset
+	c.envelopePhase = 0; // Start at attack
+	c.volume = c.attackLevel;
+
+	setChannelVolume(ch, c.volume);
+
+	debugC(3, kFreescapeDebugParser, "WB: ch%d NOTE note=%d(+%d) period=%d inst=%d env=%d vol=%d",
+		ch, c.note, c.transpose, c.basePeriod, c.instrumentIdx, c.envelopeIdx, c.volume);
+
+	// Set up portamento if active
+	if (c.portaUp || c.portaDown) {
+		int16 prevPeriod = (c.prevNote > 0 && c.prevNote <= 47) ? getPeriod(c.prevNote + c.transpose) : c.basePeriod;
+		int16 delta = ABS(c.basePeriod - prevPeriod);
+		int steps = (_tickSpeed > 0) ? _tickSpeed : 1;
+		c.portaStep = delta / steps;
+		if (c.portaStep == 0)
+			c.portaStep = 1;
+		c.portaTarget = c.basePeriod;
+		c.basePeriod = prevPeriod; // Start from previous note
+		c.outputPeriod = prevPeriod;
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Effects processing — runs every frame (50Hz)
+// Asm ref: TEXT+$05A0 (portamento), TEXT+$0620 (arpeggio/vibrato)
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::processEffects(int ch) {
+	ChannelState &c = _channels[ch];
+
+	if (c.effectMode == 0) {
+		c.outputPeriod = c.basePeriod;
+		return;
+	}
+
+	// Portamento
+	if (c.portaUp) {
+		// Sliding toward lower period (higher pitch)
+		c.basePeriod -= c.portaStep;
+		if (c.basePeriod <= c.portaTarget) {
+			c.basePeriod = c.portaTarget;
+			c.portaUp = false;
+			c.effectMode = 0;
+		}
+		c.outputPeriod = c.basePeriod;
+		return;
+	}
+
+	if (c.portaDown) {
+		// Sliding toward higher period (lower pitch)
+		c.basePeriod += c.portaStep;
+		if (c.basePeriod >= c.portaTarget) {
+			c.basePeriod = c.portaTarget;
+			c.portaDown = false;
+			c.effectMode = 0;
+		}
+		c.outputPeriod = c.basePeriod;
+		return;
+	}
+
+	// Arpeggio effect (effectMode 1 or 2)
+	if (_arpeggioTableLen > 0) {
+		int note = c.note + c.transpose;
+		int offset = _arpeggioTable[c.arpeggioPos % _arpeggioTableLen];
+		note += offset;
+		if (note < 1) note = 1;
+		if (note > 47) note = 47;
+		c.outputPeriod = getPeriod(note);
+		c.arpeggioPos++;
+		if (c.arpeggioPos >= _arpeggioTableLen)
+			c.arpeggioPos = 0;
+	} else {
+		c.outputPeriod = c.basePeriod;
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Volume envelope — runs every frame (50Hz)
+// Asm ref: TEXT+$068C (envelope processing)
+//
+// Envelope table bytes (per entry, 8 bytes at TEXT+$D0A):
+//   byte 0 (attackLevel):  initial volume on note-on
+//   byte 1 (decayTarget):  target volume to fade toward and hold
+//   byte 2 (sustainLevel): fade rate (0 = instant jump to target, >0 = per-tick)
+//   byte 3 (releaseRate):  volume decrease per tick on note-off
+//   byte 4 (modDepth):     modulation depth
+//   byte 5 (vibratoWave):  vibrato waveform selector
+//   byte 6 (arpeggioMask): bitmask into arpeggio interval table at TEXT+$D02
+//   byte 7 (flags):        misc flags
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::processEnvelope(int ch) {
+	ChannelState &c = _channels[ch];
+
+	switch (c.envelopePhase) {
+	case 0: // Attack — volume already set to attackLevel in triggerNote
+		if (c.sustainLevel == 0) {
+			// Instant transition to hold level
+			c.volume = c.decayTarget;
+			c.envelopePhase = 2; // Skip fade, go straight to hold
+		} else {
+			// Begin gradual fade toward target
+			c.envelopePhase = 1;
+		}
+		break;
+
+	case 1: // Fade — move volume toward hold level (decayTarget)
+		if (c.volume < c.decayTarget) {
+			c.volume++;
+		} else if (c.volume > c.decayTarget) {
+			c.volume--;
+		} else {
+			c.envelopePhase = 2; // Reached target
+		}
+		break;
+
+	case 2: // Hold — maintain volume until note-off
+		break;
+
+	case 3: // Release — decrease volume on note-off
+		if (c.releaseRate > 0) {
+			if (c.volume > c.releaseRate) {
+				c.volume -= c.releaseRate;
+			} else {
+				c.volume = 0;
+			}
+		} else {
+			c.volume = 0; // Rate 0 = instant off
+		}
+		break;
+	}
+
+	// Clamp to Paula range
+	if (c.volume > 64)
+		c.volume = 64;
+}
+
+// ---------------------------------------------------------------------------
+// Arpeggio table builder — unpacks a bitmask into interval offsets
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::buildArpeggioTable(byte mask) {
+	_arpeggioTableLen = 0;
+
+	// Always include 0 (base note) as first entry
+	_arpeggioTable[_arpeggioTableLen++] = 0;
+
+	for (int i = 0; i < 8 && _arpeggioTableLen < 16; i++) {
+		if (mask & (1 << i)) {
+			_arpeggioTable[_arpeggioTableLen++] = _arpeggioIntervals[i];
+		}
+	}
+
+	if (_arpeggioTableLen <= 1)
+		_arpeggioTableLen = 0; // No arpeggio if only base note
+}
+
+// ---------------------------------------------------------------------------
+// Period lookup
+// ---------------------------------------------------------------------------
+
+uint16 WallyBebenStream::getPeriod(int note) const {
+	if (note < 0 || note > 47)
+		return 0;
+	return _periods[note];
+}
+
+// ---------------------------------------------------------------------------
+// Main interrupt — called at 50Hz by Paula
+// Asm ref: TEXT+$0004 (tick entry), TEXT+$010C (sequencer body)
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::interrupt() {
+	if (!_musicActive)
+		return;
+
+	// Sequencer step: when tick counter reaches 0
+	if (_tickCounter == 0) {
+		for (int ch = 0; ch < 4; ch++) {
+			if (!_channels[ch].active)
+				continue;
+
+			if (_channels[ch].durationCounter > 0) {
+				_channels[ch].durationCounter--;
+			}
+
+			if (_channels[ch].durationCounter == 0) {
+				// Note-off: enter release phase
+				if (_channels[ch].envelopePhase < 3)
+					_channels[ch].envelopePhase = 3;
+
+				// Read next commands
+				readPatternCommands(ch);
+			}
+		}
+	}
+
+	// Every frame: process effects and envelope, update Paula
+	for (int ch = 0; ch < 4; ch++) {
+		if (!_channels[ch].active)
+			continue;
+
+		processEffects(ch);
+		processEnvelope(ch);
+
+		setChannelPeriod(ch, _channels[ch].outputPeriod);
+		setChannelVolume(ch, _channels[ch].volume);
+	}
+
+	// Advance tick counter
+	_tickCounter++;
+	if (_tickCounter >= _tickSpeed) {
+		_tickCounter = 0;
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Factory function
+// ---------------------------------------------------------------------------
+
+Audio::AudioStream *makeWallyBebenStream(const byte *data, uint32 dataSize,
+                                         int songNum, int rate, bool stereo) {
+	if (!data || dataSize < 0xF000) {
+		warning("WallyBeben: invalid data (size %u)", dataSize);
+		return nullptr;
+	}
+
+	return new WallyBebenStream(data, dataSize, songNum, rate, stereo);
+}
+
+} // End of namespace Freescape
diff --git a/engines/freescape/wb.h b/engines/freescape/wb.h
new file mode 100644
index 00000000000..1dcc22be052
--- /dev/null
+++ b/engines/freescape/wb.h
@@ -0,0 +1,47 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_WB_H
+#define FREESCAPE_WB_H
+
+#include "audio/audiostream.h"
+#include "common/types.h"
+
+namespace Freescape {
+
+/**
+ * Create a music stream for the Wally Beben custom music engine
+ * used in the Amiga version of Dark Side.
+ *
+ * @param data     Raw TEXT segment data from HDSMUSIC.AM (after 0x1C GEMDOS header)
+ * @param dataSize Size of the TEXT segment (0xF4BC for Dark Side)
+ * @param songNum  Song number to play (1 or 2)
+ * @param rate     Output sample rate
+ * @param stereo   Whether to produce stereo output
+ * @return A new AudioStream, or nullptr on error
+ */
+Audio::AudioStream *makeWallyBebenStream(const byte *data, uint32 dataSize,
+                                         int songNum = 1, int rate = 44100,
+                                         bool stereo = true);
+
+} // End of namespace Freescape
+
+#endif


Commit: c184ea0bb0b6da3d2cda308c33c91716fecb2db5
    https://github.com/scummvm/scummvm/commit/c184ea0bb0b6da3d2cda308c33c91716fecb2db5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:54+01:00

Commit Message:
FREESCAPE: load additional images from castle cpc

Changed paths:
    engines/freescape/freescape.cpp
    engines/freescape/freescape.h
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/castle.h
    engines/freescape/games/castle/cpc.cpp
    engines/freescape/games/castle/zx.cpp
    engines/freescape/games/dark/cpc.cpp
    engines/freescape/games/driller/cpc.cpp
    engines/freescape/games/eclipse/cpc.cpp
    engines/freescape/gfx.cpp
    engines/freescape/gfx.h


diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 3a08393d147..610d4f98cdb 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -39,6 +39,44 @@ namespace Freescape {
 
 FreescapeEngine *g_freescape;
 
+byte getCPCPixelMode1(byte cpc_byte, int index) {
+	if (index == 0)
+		return ((cpc_byte & 0x08) >> 2) | ((cpc_byte & 0x80) >> 7);
+	else if (index == 1)
+		return ((cpc_byte & 0x04) >> 1) | ((cpc_byte & 0x40) >> 6);
+	else if (index == 2)
+		return (cpc_byte & 0x02)        | ((cpc_byte & 0x20) >> 5);
+	else if (index == 3)
+		return ((cpc_byte & 0x01) << 1) | ((cpc_byte & 0x10) >> 4);
+	else
+		error("Invalid index %d requested", index);
+}
+
+byte getCPCPixelMode0(byte cpc_byte, int index) {
+	if (index == 0) {
+		// Extract Pixel 0 from the byte
+		return ((cpc_byte & 0x02) << 2) |  // Bit 1 -> Bit 3 (MSB)
+		       ((cpc_byte & 0x20) >> 3) |  // Bit 5 -> Bit 2
+		       ((cpc_byte & 0x08) >> 2) |  // Bit 3 -> Bit 1
+		       ((cpc_byte & 0x80) >> 7);   // Bit 7 -> Bit 0 (LSB)
+	} else if (index == 2) {
+		// Extract Pixel 1 from the byte
+		return ((cpc_byte & 0x01) << 3) |  // Bit 0 -> Bit 3 (MSB)
+		       ((cpc_byte & 0x10) >> 2) |  // Bit 4 -> Bit 2
+		       ((cpc_byte & 0x04) >> 1) |  // Bit 2 -> Bit 1
+		       ((cpc_byte & 0x40) >> 6);   // Bit 6 -> Bit 0 (LSB)
+	} else {
+		error("Invalid index %d requested", index);
+	}
+}
+
+byte getCPCPixel(byte cpc_byte, int index, bool mode1) {
+	if (mode1)
+		return getCPCPixelMode1(cpc_byte, index);
+	else
+		return getCPCPixelMode0(cpc_byte, index);
+}
+
 FreescapeEngine::FreescapeEngine(OSystem *syst, const ADGameDescription *gd)
 	: Engine(syst), _gameDescription(gd), _gfx(nullptr) {
 	if (!ConfMan.hasKey("render_mode") || ConfMan.get("render_mode").empty())
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index be13fd65f01..4e44f39eabd 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -143,6 +143,7 @@ struct CGAPaletteEntry {
 
 extern Common::String shiftStr(const Common::String &str, int shift);
 extern Common::String centerAndPadString(const Common::String &str, int size);
+extern Graphics::ManagedSurface *readCPCImage(Common::SeekableReadStream *file, bool mode1);
 
 class EventManagerWrapper {
 public:
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 8fee1961a76..76b536d0d91 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -33,6 +33,7 @@
 #include "audio/mods/protracker.h"
 
 #include "freescape/freescape.h"
+#include "freescape/gfx.h"
 #include "freescape/games/castle/castle.h"
 #include "freescape/language/8bitDetokeniser.h"
 
@@ -245,6 +246,101 @@ CastleEngine::~CastleEngine() {
 	}
 }
 
+Graphics::ManagedSurface *CastleEngine::loadFrameWithHeader(Common::SeekableReadStream *file, int pos, uint32 front, uint32 back) {
+	Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+	file->seek(pos);
+	int16 width = file->readByte();
+	int16 height = file->readByte();
+	debugC(kFreescapeDebugParser, "Frame size: %d x %d", width, height);
+	surface->create(width * 8, height, _gfx->_texturePixelFormat);
+
+	/*byte mask =*/ file->readByte();
+
+	surface->fillRect(Common::Rect(0, 0, width * 8, height), back);
+	/*int frameSize =*/ file->readUint16LE();
+	return loadFrame(file, surface, width, height, front);
+}
+
+Common::Array<Graphics::ManagedSurface *> CastleEngine::loadFramesWithHeader(Common::SeekableReadStream *file, int pos, int numFrames, uint32 front, uint32 back) {
+	Graphics::ManagedSurface *surface = nullptr;
+	file->seek(pos);
+	int16 width = file->readByte();
+	int16 height = file->readByte();
+	/*byte mask =*/ file->readByte();
+
+	/*int frameSize =*/ file->readUint16LE();
+	Common::Array<Graphics::ManagedSurface *> frames;
+	for (int i = 0; i < numFrames; i++) {
+		surface = new Graphics::ManagedSurface();
+		surface->create(width * 8, height, _gfx->_texturePixelFormat);
+		surface->fillRect(Common::Rect(0, 0, width * 8, height), back);
+		frames.push_back(loadFrame(file, surface, width, height, front));
+	}
+
+	return frames;
+}
+
+Graphics::ManagedSurface *CastleEngine::loadFrame(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, uint32 front) {
+	for (int i = 0; i < width * height; i++) {
+		byte color = file->readByte();
+		for (int n = 0; n < 8; n++) {
+			int y = i / width;
+			int x = (i % width) * 8 + (7 - n);
+			if ((color & (1 << n)))
+				surface->setPixel(x, y, front);
+		}
+	}
+	return surface;
+}
+
+Graphics::ManagedSurface *CastleEngine::loadFrameWithHeaderCPC(Common::SeekableReadStream *file, int pos, const uint32 *cpcPalette) {
+	Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+	file->seek(pos);
+	int16 width = file->readByte();
+	int16 height = file->readByte();
+	debugC(kFreescapeDebugParser, "CPC Frame size: %d x %d", width, height);
+	surface->create(width * 4, height, _gfx->_texturePixelFormat);
+
+	/*byte mask =*/ file->readByte();
+
+	surface->fillRect(Common::Rect(0, 0, width * 4, height), cpcPalette[0]);
+	/*int frameSize =*/ file->readUint16LE();
+	return loadFrameCPC(file, surface, width, height, cpcPalette);
+}
+
+Common::Array<Graphics::ManagedSurface *> CastleEngine::loadFramesWithHeaderCPC(Common::SeekableReadStream *file, int pos, int numFrames, const uint32 *cpcPalette) {
+	Graphics::ManagedSurface *surface = nullptr;
+	file->seek(pos);
+	int16 width = file->readByte();
+	int16 height = file->readByte();
+	/*byte mask =*/ file->readByte();
+
+	/*int frameSize =*/ file->readUint16LE();
+	Common::Array<Graphics::ManagedSurface *> frames;
+	for (int i = 0; i < numFrames; i++) {
+		surface = new Graphics::ManagedSurface();
+		surface->create(width * 4, height, _gfx->_texturePixelFormat);
+		surface->fillRect(Common::Rect(0, 0, width * 4, height), cpcPalette[0]);
+		frames.push_back(loadFrameCPC(file, surface, width, height, cpcPalette));
+	}
+
+	return frames;
+}
+
+Graphics::ManagedSurface *CastleEngine::loadFrameCPC(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, const uint32 *cpcPalette) {
+	for (int y = 0; y < height; y++) {
+		for (int col = 0; col < width; col++) {
+			byte cpc_byte = file->readByte();
+			for (int i = 0; i < 4; i++) {
+				int pixel = getCPCPixel(cpc_byte, i, true);
+				if (pixel != 0)
+					surface->setPixel(col * 4 + i, y, cpcPalette[pixel]);
+			}
+		}
+	}
+	return surface;
+}
+
 void CastleEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target) {
 	FreescapeEngine::initKeymaps(engineKeyMap, infoScreenKeyMap, target);
 	Common::Action *act;
@@ -1224,12 +1320,20 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
 		y = 33;
 		maxWidth = 139;
 	}
-	surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, x, y, Common::Rect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h));
-	for (y += _riddleTopFrame->h; y < maxWidth;) {
-		surface->copyRectToSurface((const Graphics::Surface)*_riddleBackgroundFrame, x, y, Common::Rect(0, 0, _riddleBackgroundFrame->w, _riddleBackgroundFrame->h));
-		y += _riddleBackgroundFrame->h;
+	// Draw riddle frame borders (if available)
+	if (_riddleTopFrame) {
+		surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, x, y, Common::Rect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h));
+		y += _riddleTopFrame->h;
+	}
+	if (_riddleBackgroundFrame) {
+		for (; y < maxWidth;) {
+			surface->copyRectToSurface((const Graphics::Surface)*_riddleBackgroundFrame, x, y, Common::Rect(0, 0, _riddleBackgroundFrame->w, _riddleBackgroundFrame->h));
+			y += _riddleBackgroundFrame->h;
+		}
+	}
+	if (_riddleBottomFrame) {
+		surface->copyRectToSurface((const Graphics::Surface)*_riddleBottomFrame, x, maxWidth, Common::Rect(0, 0, _riddleBottomFrame->w, _riddleBottomFrame->h - 1));
 	}
-	surface->copyRectToSurface((const Graphics::Surface)*_riddleBottomFrame, x, maxWidth, Common::Rect(0, 0, _riddleBottomFrame->w, _riddleBottomFrame->h - 1));
 
 	Common::Array<RiddleText> riddleMessages = _riddleList[riddle]._lines;
 	x = _riddleList[riddle]._origin.x;
@@ -1286,41 +1390,48 @@ void CastleEngine::drawEnergyMeter(Graphics::Surface *surface, Common::Point ori
 		barFrameOrigin += Common::Point(5, 6);
 	else if (isSpectrum())
 		barFrameOrigin += Common::Point(0, 6);
+	else if (isCPC())
+		barFrameOrigin += Common::Point(0, 6);
 
 	surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtBarFrame, barFrameOrigin.x, barFrameOrigin.y, Common::Rect(0, 0, _strenghtBarFrame->w, _strenghtBarFrame->h), black);
 
 	Common::Point weightPoint;
 	int frameIdx = -1;
 
-	weightPoint = Common::Point(origin.x + 10, origin.y);
-	frameIdx = _gameStateVars[k8bitVariableShield] % 4;
-
 	if (_strenghtWeightsFrames.empty())
 		return;
 
+	// Use actual weight sprite width for positioning
+	int weightWidth = _strenghtWeightsFrames[0]->w;
+	int weightOffset = isCPC() ? 9 : 10;
+	int rightWeightPos = isCPC() ? 59 : 62;
+
+	weightPoint = Common::Point(origin.x + weightOffset, origin.y);
+	frameIdx = _gameStateVars[k8bitVariableShield] % 4;
+
 	if (frameIdx != 0) {
 		frameIdx = 4 - frameIdx;
-		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[frameIdx], weightPoint.x, weightPoint.y, Common::Rect(0, 0, 3, _strenghtWeightsFrames[frameIdx]->h), back);
-		weightPoint += Common::Point(3, 0);
+		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[frameIdx], weightPoint.x, weightPoint.y, Common::Rect(0, 0, weightWidth, _strenghtWeightsFrames[frameIdx]->h), back);
+		weightPoint += Common::Point(weightWidth, 0);
 	}
 
 	for (int i = 0; i < _gameStateVars[k8bitVariableShield] / 4; i++) {
-		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[0], weightPoint.x, weightPoint.y, Common::Rect(0, 0, 3, _strenghtWeightsFrames[0]->h), back);
-		weightPoint += Common::Point(3, 0);
+		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[0], weightPoint.x, weightPoint.y, Common::Rect(0, 0, weightWidth, _strenghtWeightsFrames[0]->h), back);
+		weightPoint += Common::Point(weightWidth, 0);
 	}
 
-	weightPoint = Common::Point(origin.x + 62, origin.y);
+	weightPoint = Common::Point(origin.x + rightWeightPos, origin.y);
 	frameIdx = _gameStateVars[k8bitVariableShield] % 4;
 
 	if (frameIdx != 0) {
 		frameIdx = 4 - frameIdx;
-		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[frameIdx], weightPoint.x, weightPoint.y, Common::Rect(0, 0, 3, _strenghtWeightsFrames[frameIdx]->h), back);
-		weightPoint += Common::Point(-3, 0);
+		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[frameIdx], weightPoint.x, weightPoint.y, Common::Rect(0, 0, weightWidth, _strenghtWeightsFrames[frameIdx]->h), back);
+		weightPoint += Common::Point(-weightWidth, 0);
 	}
 
 	for (int i = 0; i < _gameStateVars[k8bitVariableShield] / 4; i++) {
-		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[0], weightPoint.x, weightPoint.y, Common::Rect(0, 0, 3, _strenghtWeightsFrames[0]->h), back);
-		weightPoint += Common::Point(-3, 0);
+		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[0], weightPoint.x, weightPoint.y, Common::Rect(0, 0, weightWidth, _strenghtWeightsFrames[0]->h), back);
+		weightPoint += Common::Point(-weightWidth, 0);
 	}
 }
 
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 815f605e5da..2f54595a9f3 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -103,6 +103,13 @@ public:
 	Common::Array<Graphics::ManagedSurface *> loadFramesWithHeader(Common::SeekableReadStream *file, int pos, int numFrames, uint32 front, uint32 back);
 	Graphics::ManagedSurface *loadFrameWithHeader(Common::SeekableReadStream *file, int pos, uint32 front, uint32 back);
 	Graphics::ManagedSurface *loadFrame(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, uint32 back);
+
+	// CPC-specific frame loading (Mode 1: 4 pixels per byte)
+	// cpcPalette is a 4-entry array mapping CPC ink numbers (0-3) to ARGB colors
+	Common::Array<Graphics::ManagedSurface *> loadFramesWithHeaderCPC(Common::SeekableReadStream *file, int pos, int numFrames, const uint32 *cpcPalette);
+	Graphics::ManagedSurface *loadFrameWithHeaderCPC(Common::SeekableReadStream *file, int pos, const uint32 *cpcPalette);
+	Graphics::ManagedSurface *loadFrameCPC(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, const uint32 *cpcPalette);
+
 	Graphics::ManagedSurface *loadFrameFromPlanes(Common::SeekableReadStream *file, int widthInBytes, int height);
 	Graphics::ManagedSurface *loadFrameFromPlanesInternal(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height);
 
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index bd149ea3ca9..a1d1bb1374e 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -96,7 +96,7 @@ byte mountainsData[288] {
 	0xaa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
 };
 
-extern Graphics::ManagedSurface *readCPCImage(Common::SeekableReadStream *file, bool mode0);
+
 
 void CastleEngine::loadAssetsCPCFullGame() {
 	Common::File file;
@@ -178,74 +178,43 @@ void CastleEngine::loadAssetsCPCFullGame() {
 	uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
 
 	_background = loadFrame(&mountainsStream, background, backgroundWidth, backgroundHeight, front);
-	/*_gfx->readFromPalette(2, r, g, b);
-	uint32 red = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
 
-	_gfx->readFromPalette(7, r, g, b);
-	uint32 white = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+	// CPC UI Sprites - located at different offsets than ZX Spectrum!
+	// CPC uses Mode 1 format (4 pixels per byte, 2 bits per pixel).
+	// Sprite pixel values 0-3 are CPC ink numbers that map to the border palette.
+	uint32 cpcPalette[4];
+	for (int i = 0; i < 4; i++) {
+		cpcPalette[i] = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
+			kCPCPaletteCastleBorderData[i][0],
+			kCPCPaletteCastleBorderData[i][1],
+			kCPCPaletteCastleBorderData[i][2]);
+	}
 
-	_keysBorderFrames.push_back(loadFrameWithHeader(&file, _language == Common::ES_ESP ? 0xe06 : 0xdf7, red, white));
+	// Keys Border: CPC offset 0x2362 (8x14 px, 1 frame - matches ZX key_sprite)
+	_keysBorderFrames.push_back(loadFrameWithHeaderCPC(&file, 0x2362, cpcPalette));
 
-	uint32 green = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0xff, 0);
-	_spiritsMeterIndicatorFrame = loadFrameWithHeader(&file, _language == Common::ES_ESP ? 0xe5e : 0xe4f, green, white);
+	// Spirit Meter Background: CPC offset 0x2383 (64x8 px - matches ZX spirit_meter_bg)
+	_spiritsMeterIndicatorBackgroundFrame = loadFrameWithHeaderCPC(&file, 0x2383, cpcPalette);
 
-	_gfx->readFromPalette(4, r, g, b);
-	uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+	// Spirit Meter Indicator: CPC offset 0x2408 (16x8 px - matches ZX spirit_meter_indicator)
+	_spiritsMeterIndicatorFrame = loadFrameWithHeaderCPC(&file, 0x2408, cpcPalette);
 
-	int backgroundWidth = 16;
-	int backgroundHeight = 18;
-	Graphics::ManagedSurface *background = new Graphics::ManagedSurface();
-	background->create(backgroundWidth * 8, backgroundHeight, _gfx->_texturePixelFormat);
-	background->fillRect(Common::Rect(0, 0, backgroundWidth * 8, backgroundHeight), 0);
+	// Strength Background: CPC offset 0x242D (68x15 px - matches ZX strength_bg)
+	_strenghtBackgroundFrame = loadFrameWithHeaderCPC(&file, 0x242D, cpcPalette);
 
-	file.seek(_language == Common::ES_ESP ? 0xfd3 : 0xfc4);
-	_background = loadFrame(&file, background, backgroundWidth, backgroundHeight, front);
-
-	_gfx->readFromPalette(6, r, g, b);
-	uint32 yellow = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
-	uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
-	_strenghtBackgroundFrame = loadFrameWithHeader(&file, _language == Common::ES_ESP ? 0xee6 : 0xed7, yellow, black);
-	_strenghtBarFrame = loadFrameWithHeader(&file, _language == Common::ES_ESP ? 0xf72 : 0xf63, yellow, black);
-
-	Graphics::ManagedSurface *bar = new Graphics::ManagedSurface();
-	bar->create(_strenghtBarFrame->w - 4, _strenghtBarFrame->h, _gfx->_texturePixelFormat);
-	_strenghtBarFrame->copyRectToSurface(*bar, 4, 0, Common::Rect(4, 0, _strenghtBarFrame->w - 4, _strenghtBarFrame->h));
-	_strenghtBarFrame->free();
-	delete _strenghtBarFrame;
-	_strenghtBarFrame = bar;
-
-	_strenghtWeightsFrames = loadFramesWithHeader(&file, _language == Common::ES_ESP ? 0xf92 : 0xf83, 4, yellow, black);
-
-	_flagFrames = loadFramesWithHeader(&file, (_language == Common::ES_ESP ? 0x10e4 + 15 : 0x10e4), 4, green, black);
-
-	int thunderWidth = 4;
-	int thunderHeight = 43;
-	_thunderFrame = new Graphics::ManagedSurface();
-	_thunderFrame->create(thunderWidth * 8, thunderHeight, _gfx->_texturePixelFormat);
-	_thunderFrame->fillRect(Common::Rect(0, 0, thunderWidth * 8, thunderHeight), 0);
-	_thunderFrame = loadFrame(&file, _thunderFrame, thunderWidth, thunderHeight, front);
-
-	Graphics::Surface *tmp;
-	tmp = loadBundledImage("castle_riddle_top_frame");
-	_riddleTopFrame = new Graphics::ManagedSurface;
-	_riddleTopFrame->copyFrom(*tmp);
-	tmp->free();
-	delete tmp;
-	_riddleTopFrame->convertToInPlace(_gfx->_texturePixelFormat);
-
-	tmp = loadBundledImage("castle_riddle_background_frame");
-	_riddleBackgroundFrame = new Graphics::ManagedSurface();
-	_riddleBackgroundFrame->copyFrom(*tmp);
-	tmp->free();
-	delete tmp;
-	_riddleBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat);
-
-	tmp = loadBundledImage("castle_riddle_bottom_frame");
-	_riddleBottomFrame = new Graphics::ManagedSurface();
-	_riddleBottomFrame->copyFrom(*tmp);
-	tmp->free();
-	delete tmp;
-	_riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat);*/
+	// Strength Bar: CPC offset 0x2531 (68x3 px - matches ZX strength_bar)
+	_strenghtBarFrame = loadFrameWithHeaderCPC(&file, 0x2531, cpcPalette);
+
+	// Strength Weights: CPC offset 0x2569 (4x15 px, 4 frames - matches ZX weight_sprite w=1,h=15)
+	_strenghtWeightsFrames = loadFramesWithHeaderCPC(&file, 0x2569, 4, cpcPalette);
+
+	// Flag Animation: CPC offset 0x2654 (16x9 px, 4 frames)
+	_flagFrames = loadFramesWithHeaderCPC(&file, 0x2654, 4, cpcPalette);
+
+	// Note: Riddle frames are not loaded from external files for CPC.
+	// The _riddleTopFrame, _riddleBackgroundFrame, and _riddleBottomFrame
+	// will remain nullptr (initialized in constructor).
+	// The drawRiddle function handles nullptr gracefully.
 
 	for (auto &it : _areaMap) {
 		it._value->addStructure(_areaMap[255]);
@@ -273,7 +242,6 @@ void CastleEngine::loadAssetsCPCFullGame() {
 
 void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
 	uint32 color = _gfx->_paperColor;
-	//uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
 	uint8 r, g, b;
 
 	_gfx->readFromPalette(color, r, g, b);
@@ -296,50 +264,42 @@ void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
 		_temporaryMessageDeadlines.push_back(deadline);
 	} else {
 		if (_gameStateControl == kFreescapeGameStatePlaying) {
-				drawStringInSurface(_currentArea->_name, 97, 182, front, back, surface);
+			drawStringInSurface(_currentArea->_name, 97, 182, front, back, surface);
 		}
 	}
 
-	/*uint32 color = 5;
-	uint8 r, g, b;
-
-	_gfx->readFromPalette(color, r, g, b);
-	uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
-
-	color = 0;
-	_gfx->readFromPalette(color, r, g, b);
-	uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+	// Draw collected keys
+	if (!_keysBorderFrames.empty()) {
+		for (int k = 0; k < int(_keysCollected.size()); k++) {
+			surface->copyRectToSurface((const Graphics::Surface)*_keysBorderFrames[0], 76 - k * 4, 179, Common::Rect(0, 0, _keysBorderFrames[0]->w, _keysBorderFrames[0]->h));
+		}
+	}
 
-	Common::Rect backRect(123, 179, 242 + 5, 188);
-	surface->fillRect(backRect, black);
+	// Draw energy meter (strength)
+	drawEnergyMeter(surface, Common::Point(38, 158));
 
-	Common::String message;
-	int deadline = -1;
-	getLatestMessages(message, deadline);
-	if (deadline > 0 && deadline <= _countdown) {
-		//debug("deadline: %d countdown: %d", deadline, _countdown);
-		drawStringInSurface(message, 120, 179, front, black, surface);
-		_temporaryMessages.push_back(message);
-		_temporaryMessageDeadlines.push_back(deadline);
+	// Draw spirit meter
+	uint32 blackColor = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
+	if (_spiritsMeterIndicatorBackgroundFrame) {
+		surface->copyRectToSurface((const Graphics::Surface)*_spiritsMeterIndicatorBackgroundFrame, 136, 162, Common::Rect(0, 0, _spiritsMeterIndicatorBackgroundFrame->w, _spiritsMeterIndicatorBackgroundFrame->h));
+		if (_spiritsMeterIndicatorFrame) {
+			surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_spiritsMeterIndicatorFrame, 131 + _spiritsMeterPosition, 161, Common::Rect(0, 0, _spiritsMeterIndicatorFrame->w, _spiritsMeterIndicatorFrame->h), blackColor);
+		}
 	} else {
-		if (_gameStateControl == kFreescapeGameStatePlaying) {
-			drawStringInSurface(_currentArea->_name, 120, 179, front, black, surface);
+		uint32 green = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0xff, 0x00);
+		surface->fillRect(Common::Rect(152, 162, 216, 170), green);
+		if (_spiritsMeterIndicatorFrame) {
+			surface->copyRectToSurface((const Graphics::Surface)*_spiritsMeterIndicatorFrame, 131 + _spiritsMeterPosition, 161, Common::Rect(0, 0, _spiritsMeterIndicatorFrame->w, _spiritsMeterIndicatorFrame->h));
 		}
 	}
 
-	for (int k = 0; k < int(_keysCollected.size()); k++) {
-		surface->copyRectToSurface((const Graphics::Surface)*_keysBorderFrames[0], 99 - k * 4, 177, Common::Rect(0, 0, 6, 11));
+	// Draw animated flag
+	if (!_flagFrames.empty()) {
+		int ticks = g_system->getMillis() / 20;
+		int flagFrameIndex = (ticks / 10) % 4;
+		surface->copyRectToSurface(*_flagFrames[flagFrameIndex], 285, 5, Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));
 	}
 
-	uint32 green = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0xff, 0);
-
-	surface->fillRect(Common::Rect(152, 156, 216, 164), green);
-	surface->copyRectToSurface((const Graphics::Surface)*_spiritsMeterIndicatorFrame, 140 + _spiritsMeterPosition, 156, Common::Rect(0, 0, 15, 8));
-	drawEnergyMeter(surface, Common::Point(63, 154));
-
-	int ticks = g_system->getMillis() / 20;
-	int flagFrameIndex = (ticks / 10) % 4;
-	surface->copyRectToSurface(*_flagFrames[flagFrameIndex], 264, 9, Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));*/
 }
 
 } // End of namespace Freescape
diff --git a/engines/freescape/games/castle/zx.cpp b/engines/freescape/games/castle/zx.cpp
index 6eb5354714b..1aaaf37408a 100644
--- a/engines/freescape/games/castle/zx.cpp
+++ b/engines/freescape/games/castle/zx.cpp
@@ -41,54 +41,6 @@ void CastleEngine::initZX() {
 	_soundIndexAreaChange = 7;
 }
 
-Graphics::ManagedSurface *CastleEngine::loadFrameWithHeader(Common::SeekableReadStream *file, int pos, uint32 front, uint32 back) {
-	Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
-	file->seek(pos);
-	int16 width = file->readByte();
-	int16 height = file->readByte();
-	debugC(kFreescapeDebugParser, "Frame size: %d x %d", width, height);
-	surface->create(width * 8, height, _gfx->_texturePixelFormat);
-
-	/*byte mask =*/ file->readByte();
-
-	surface->fillRect(Common::Rect(0, 0, width * 8, height), back);
-	/*int frameSize =*/ file->readUint16LE();
-	return loadFrame(file, surface, width, height, front);
-}
-
-Common::Array<Graphics::ManagedSurface *> CastleEngine::loadFramesWithHeader(Common::SeekableReadStream *file, int pos, int numFrames, uint32 front, uint32 back) {
-	Graphics::ManagedSurface *surface = nullptr;
-	file->seek(pos);
-	int16 width = file->readByte();
-	int16 height = file->readByte();
-	/*byte mask =*/ file->readByte();
-
-	/*int frameSize =*/ file->readUint16LE();
-	Common::Array<Graphics::ManagedSurface *> frames;
-	for (int i = 0; i < numFrames; i++) {
-		surface = new Graphics::ManagedSurface();
-		surface->create(width * 8, height, _gfx->_texturePixelFormat);
-		surface->fillRect(Common::Rect(0, 0, width * 8, height), back);
-		frames.push_back(loadFrame(file, surface, width, height, front));
-	}
-
-	return frames;
-}
-
-
-Graphics::ManagedSurface *CastleEngine::loadFrame(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, uint32 front) {
-	for (int i = 0; i < width * height; i++) {
-		byte color = file->readByte();
-		for (int n = 0; n < 8; n++) {
-			int y = i / width;
-			int x = (i % width) * 8 + (7 - n);
-			if ((color & (1 << n)))
-				surface->setPixel(x, y, front);
-		}
-	}
-	return surface;
-}
-
 void CastleEngine::loadAssetsZXFullGame() {
 	Common::File file;
 	uint8 r, g, b;
diff --git a/engines/freescape/games/dark/cpc.cpp b/engines/freescape/games/dark/cpc.cpp
index 5df3839a54e..c29748f5178 100644
--- a/engines/freescape/games/dark/cpc.cpp
+++ b/engines/freescape/games/dark/cpc.cpp
@@ -60,7 +60,6 @@ byte kCPCPaletteDarkTitle[16][3] = {
 	{0x00, 0x80, 0x00}, // 15: X
 };
 
-extern Graphics::ManagedSurface *readCPCImage(Common::SeekableReadStream *file, bool mode0);
 
 void DarkEngine::loadAssetsCPCFullGame() {
 	Common::File file;
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 404fc20da4f..a0cda34d34f 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -64,45 +64,7 @@ byte kCPCPaletteBorderData[4][3] = {
 	{0x00, 0x80, 0x00},
 };
 
-byte getCPCPixelMode1(byte cpc_byte, int index) {
-	if (index == 0)
-		return ((cpc_byte & 0x08) >> 2) | ((cpc_byte & 0x80) >> 7);
-	else if (index == 1)
-		return ((cpc_byte & 0x04) >> 1) | ((cpc_byte & 0x40) >> 6);
-	else if (index == 2)
-		return (cpc_byte & 0x02)        | ((cpc_byte & 0x20) >> 5);
-	else if (index == 3)
-		return ((cpc_byte & 0x01) << 1) | ((cpc_byte & 0x10) >> 4);
-	else
-		error("Invalid index %d requested", index);
-}
-
-byte getCPCPixelMode0(byte cpc_byte, int index) {
-    if (index == 0) {
-        // Extract Pixel 0 from the byte
-        return ((cpc_byte & 0x02) << 2) |  // Bit 1 -> Bit 3 (MSB)
-               ((cpc_byte & 0x20) >> 3) |  // Bit 5 -> Bit 2
-               ((cpc_byte & 0x08) >> 2) |  // Bit 3 -> Bit 1
-               ((cpc_byte & 0x80) >> 7);   // Bit 7 -> Bit 0 (LSB)
-    }
-    else if (index == 2) {
-        // Extract Pixel 1 from the byte
-        return ((cpc_byte & 0x01) << 3) |  // Bit 0 -> Bit 3 (MSB)
-               ((cpc_byte & 0x10) >> 2) |  // Bit 4 -> Bit 2
-               ((cpc_byte & 0x04) >> 1) |  // Bit 2 -> Bit 1
-               ((cpc_byte & 0x40) >> 6);   // Bit 6 -> Bit 0 (LSB)
-    }
-    else {
-        error("Invalid index %d requested", index);
-    }
-}
-
-byte getCPCPixel(byte cpc_byte, int index, bool mode1) {
-	if (mode1)
-		return getCPCPixelMode1(cpc_byte, index);
-	else
-		return getCPCPixelMode0(cpc_byte, index);
-}
+// getCPCPixelMode1, getCPCPixelMode0, and getCPCPixel moved to freescape.cpp
 
 Graphics::ManagedSurface *readCPCImage(Common::SeekableReadStream *file, bool mode1) {
 	Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
diff --git a/engines/freescape/games/eclipse/cpc.cpp b/engines/freescape/games/eclipse/cpc.cpp
index c7683addc7c..0f4dd244126 100644
--- a/engines/freescape/games/eclipse/cpc.cpp
+++ b/engines/freescape/games/eclipse/cpc.cpp
@@ -62,7 +62,6 @@ byte kCPCPaletteEclipseBorderData[4][3] = {
 };
 
 
-extern Graphics::ManagedSurface *readCPCImage(Common::SeekableReadStream *file, bool mode0);
 
 void EclipseEngine::loadAssetsCPCFullGame() {
 	Common::File file;
diff --git a/engines/freescape/gfx.cpp b/engines/freescape/gfx.cpp
index c777b01b514..83410420e9a 100644
--- a/engines/freescape/gfx.cpp
+++ b/engines/freescape/gfx.cpp
@@ -69,8 +69,6 @@ Renderer::Renderer(int screenW, int screenH, Common::RenderMode renderMode, bool
 
 Renderer::~Renderer() {}
 
-extern byte getCPCPixel(byte cpc_byte, int index, bool mode0);
-
 byte getCPCStipple(byte cpc_byte, int back, int fore) {
 	int c0 = getCPCPixel(cpc_byte, 0, true);
 	assert(c0 == back || c0 == fore);
diff --git a/engines/freescape/gfx.h b/engines/freescape/gfx.h
index a375eac9b27..8f9ae18fb78 100644
--- a/engines/freescape/gfx.h
+++ b/engines/freescape/gfx.h
@@ -43,6 +43,9 @@ typedef Common::HashMap<int, int> ColorReMap;
 class Renderer;
 
 const Graphics::PixelFormat getRGBAPixelFormat();
+byte getCPCPixelMode1(byte cpc_byte, int index);
+byte getCPCPixelMode0(byte cpc_byte, int index);
+byte getCPCPixel(byte cpc_byte, int index, bool mode1);
 
 class Texture {
 public:


Commit: 6f27dba36dd68f6ddb004aea8b20e66a0e37c81c
    https://github.com/scummvm/scummvm/commit/6f27dba36dd68f6ddb004aea8b20e66a0e37c81c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:55+01:00

Commit Message:
FREESCAPE: load additional images from castle amiga

Changed paths:
    engines/freescape/games/castle/amiga.cpp
    engines/freescape/games/castle/castle.cpp


diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 7588a98c7f1..f0af67820e7 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -217,6 +217,99 @@ void CastleEngine::loadAssetsAmigaDemo() {
 	_riddleBottomFrame = loadFrameFromPlanesInterleaved(&file, 16, 8);
 	_riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
 
+	// Castle gate (game over background frame)
+	// Pixel data: 43 rows × 96 bytes (16 columns × 3 words) at file 0x39AE2
+	// Mask data: 43 rows × 32 bytes (16 words) at file 0x3AB02
+	// FUN_2CCA tiles 24 top rows + 19 bottom rows into a 256×120 gate image
+	{
+		static const int kTopRows = 24;
+		static const int kBottomRows = 19;
+		static const int kTotalSrcRows = kTopRows + kBottomRows;
+		static const int kColumnsPerRow = 16;
+		static const int kPixelBytesPerRow = kColumnsPerRow * 6; // 3 words × 2 bytes
+		static const int kMaskBytesPerRow = kColumnsPerRow * 2;  // 1 word × 2 bytes
+		static const int kGateWidth = 256;
+		static const int kGateHeight = 120;
+
+		byte pixelData[kTotalSrcRows * kPixelBytesPerRow];
+		byte maskData[kTotalSrcRows * kMaskBytesPerRow];
+
+		file.seek(0x39AE2);
+		file.read(pixelData, sizeof(pixelData));
+		file.seek(0x3AB02);
+		file.read(maskData, sizeof(maskData));
+
+		uint32 keyColor = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x24, 0xA5);
+		uint32 paletteColors[8];
+		for (int i = 0; i < 8; i++)
+			paletteColors[i] = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
+				kAmigaCastlePalette[i][0], kAmigaCastlePalette[i][1], kAmigaCastlePalette[i][2]);
+
+		_gameOverBackgroundFrame = new Graphics::ManagedSurface();
+		_gameOverBackgroundFrame->create(kGateWidth, kGateHeight, _gfx->_texturePixelFormat);
+		_gameOverBackgroundFrame->fillRect(Common::Rect(0, 0, kGateWidth, kGateHeight), keyColor);
+
+		// Build row mapping: FUN_2CCA tiling for N=120
+		// 5 tail rows from top portion (rows 19-23), then 4×24 full top blocks, then 19 bottom rows
+		int destRow = 0;
+		// Tail of top portion
+		for (int r = kTopRows - 5; r < kTopRows; r++) {
+			int srcRow = r;
+			for (int col = 0; col < kColumnsPerRow; col++) {
+				uint16 mask = READ_BE_UINT16(&maskData[srcRow * kMaskBytesPerRow + col * 2]);
+				int pOff = srcRow * kPixelBytesPerRow + col * 6;
+				uint16 p0 = READ_BE_UINT16(&pixelData[pOff]);
+				uint16 p1 = READ_BE_UINT16(&pixelData[pOff + 2]);
+				uint16 p2 = READ_BE_UINT16(&pixelData[pOff + 4]);
+				for (int bit = 15; bit >= 0; bit--) {
+					if (!(mask & (1 << bit))) {
+						int color = ((p0 >> bit) & 1) | (((p1 >> bit) & 1) << 1) | (((p2 >> bit) & 1) << 2);
+						_gameOverBackgroundFrame->setPixel(col * 16 + (15 - bit), destRow, paletteColors[color]);
+					}
+				}
+			}
+			destRow++;
+		}
+		// 4 full repetitions of top portion (24 rows each)
+		for (int block = 0; block < 4; block++) {
+			for (int r = 0; r < kTopRows; r++) {
+				int srcRow = r;
+				for (int col = 0; col < kColumnsPerRow; col++) {
+					uint16 mask = READ_BE_UINT16(&maskData[srcRow * kMaskBytesPerRow + col * 2]);
+					int pOff = srcRow * kPixelBytesPerRow + col * 6;
+					uint16 p0 = READ_BE_UINT16(&pixelData[pOff]);
+					uint16 p1 = READ_BE_UINT16(&pixelData[pOff + 2]);
+					uint16 p2 = READ_BE_UINT16(&pixelData[pOff + 4]);
+					for (int bit = 15; bit >= 0; bit--) {
+						if (!(mask & (1 << bit))) {
+							int color = ((p0 >> bit) & 1) | (((p1 >> bit) & 1) << 1) | (((p2 >> bit) & 1) << 2);
+							_gameOverBackgroundFrame->setPixel(col * 16 + (15 - bit), destRow, paletteColors[color]);
+						}
+					}
+				}
+				destRow++;
+			}
+		}
+		// Bottom portion (19 rows)
+		for (int r = 0; r < kBottomRows; r++) {
+			int srcRow = kTopRows + r;
+			for (int col = 0; col < kColumnsPerRow; col++) {
+				uint16 mask = READ_BE_UINT16(&maskData[srcRow * kMaskBytesPerRow + col * 2]);
+				int pOff = srcRow * kPixelBytesPerRow + col * 6;
+				uint16 p0 = READ_BE_UINT16(&pixelData[pOff]);
+				uint16 p1 = READ_BE_UINT16(&pixelData[pOff + 2]);
+				uint16 p2 = READ_BE_UINT16(&pixelData[pOff + 4]);
+				for (int bit = 15; bit >= 0; bit--) {
+					if (!(mask & (1 << bit))) {
+						int color = ((p0 >> bit) & 1) | (((p1 >> bit) & 1) << 1) | (((p2 >> bit) & 1) << 2);
+						_gameOverBackgroundFrame->setPixel(col * 16 + (15 - bit), destRow, paletteColors[color]);
+					}
+				}
+			}
+			destRow++;
+		}
+	}
+
 	// Load embedded ProTracker module for background music
 	// Module is at file offset 0x3D5A6 (memory 0x3D58A), ~86260 bytes
 	static const int kModOffset = 0x3D5A6;
@@ -237,6 +330,9 @@ void CastleEngine::loadAssetsAmigaDemo() {
 }
 
 void CastleEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
+	drawLiftingGate(surface);
+	drawDroppingGate(surface);
+
 	drawStringInSurface(_currentArea->_name, 97, 182, 0, 0, surface);
 	uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
 
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 76b536d0d91..53b150ba284 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -421,6 +421,8 @@ void CastleEngine::beforeStarting() {
 		waitInLoop(250);
 	else if (isSpectrum())
 		waitInLoop(100);
+	else if (isAmiga() || isAtariST())
+		waitInLoop(250);
 }
 
 void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
@@ -1696,6 +1698,8 @@ void CastleEngine::drawLiftingGate(Graphics::Surface *surface) {
 		duration = 250;
 	else if (isSpectrum())
 		duration = 100;
+	else if (isAmiga() || isAtariST())
+		duration = 250;
 
 	if ((_gameStateControl == kFreescapeGameStateStart || _gameStateControl == kFreescapeGameStateRestart) && _ticks <= duration) { // Draw the _gameOverBackgroundFrame gate lifting up slowly
 		int gate_w = _gameOverBackgroundFrame->w;


Commit: d92aaeb1cd7a8f69c593fe35451f2ca7f6496e4a
    https://github.com/scummvm/scummvm/commit/d92aaeb1cd7a8f69c593fe35451f2ca7f6496e4a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:55+01:00

Commit Message:
FREESCAPE: add sound for castle cpc

Changed paths:
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/cpc.cpp


diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 53b150ba284..e377951a8fd 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -1002,7 +1002,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
 
 	if (isDOS()) {
 		// TODO: playSound(X, false, _soundFxHandle);
-	} else if (isSpectrum()) {
+	} else if (isSpectrum() || isCPC()) {
 		playSound(9, false, _soundFxHandle);
 	}
 
@@ -1849,7 +1849,7 @@ void CastleEngine::updateThunder() {
 		_gfx->drawThunder(_thunderTextures[0], _position + _thunderOffset, 100);
 		_thunderFrameDuration--;
 		if (_thunderFrameDuration == 0)
-			if (isSpectrum())
+			if (isSpectrum() || isCPC())
 				playSound(8, false, _soundFxHandle);
 		return;
 	}
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index a1d1bb1374e..f0e86d68a64 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -30,14 +30,33 @@ namespace Freescape {
 
 void CastleEngine::initCPC() {
 	_viewArea = Common::Rect(40, 33 - 2, 280, 152);
-	_soundIndexShoot = 5;
-	_soundIndexCollide = -1;
-	_soundIndexFallen = -1;
-	_soundIndexStepUp = -1;
-	_soundIndexStepDown = -1;
-	_soundIndexMenu = -1;
-	_soundIndexStart = 6;
-	_soundIndexAreaChange = 7;
+
+	// Sound indices verified from Z80 disassembly of CM.BIN.
+	// The CPC version uses the same sound IDs as the ZX Spectrum version
+	// (see castlemaster2-annotated-zx-spectrum.asm for SFX constant names).
+	//
+	// Direct calls in the binary:
+	//   ld a,5 / call 0x786F  at 0x6E5D  -> shoot/throw rock (SFX_THROW_ROCK_OR_LAND)
+	//   ld a,3 / call 0x786F  at 0x25E5+  -> menu select / collision (SFX_MENU_SELECT)
+	//   ld a,9 / call z,0x786F at 0x88F1  -> gate close (SFX_GATE_CLOSE)
+	//   ld a,15 / call 0x786F at 0x8921  -> end game sequence
+	//
+	// Deferred via _requested_SFX variable (0xCFD9):
+	//   ld a,12 / ld (0xCFD9),a at 0x62A5, 0x634A -> step up/down (SFX_CLIMB_DROP)
+	//   ld a,7  / ld (0xCFD9),a at 0x7554          -> area change/teleport (SFX_GAME_START)
+	//   ld a,6  / ld (0xCFD9),a at 0x8625          -> falling (SFX_FALLING)
+	//   ld a,8  / ld (0xCFD9),a at 0x864E          -> lightning/landing (SFX_LIGHTNING)
+	//   ld a,13 / ld (0xCFD9),a at 0x7597          -> escaped (SFX_OPEN_ESCAPED)
+	_soundIndexShoot = 5;            // SFX_THROW_ROCK_OR_LAND
+	_soundIndexCollide = 3;          // SFX_MENU_SELECT (also used for collisions)
+	_soundIndexFall = 6;             // SFX_FALLING (during fall)
+	_soundIndexFallen = 5;           // SFX_THROW_ROCK_OR_LAND (landing after fall)
+	_soundIndexStartFalling = -1;    // Not used separately in Castle CPC
+	_soundIndexStepUp = 12;          // SFX_CLIMB_DROP
+	_soundIndexStepDown = 12;        // SFX_CLIMB_DROP
+	_soundIndexMenu = 3;             // SFX_MENU_SELECT
+	_soundIndexStart = 7;            // SFX_GAME_START
+	_soundIndexAreaChange = 7;       // SFX_GAME_START (same as ZX)
 }
 
 
@@ -148,6 +167,7 @@ void CastleEngine::loadAssetsCPCFullGame() {
 		case Common::EN_ANY:
 			loadRiddles(&file, 0x1b75 - 2 - 9 * 2, 9);
 			load8bitBinary(&file, 0x791a, 16);
+			loadSoundsCPC(&file, 0x21E2, 48, 0x2212, 204, 0x2179, 105);
 
 			file.seek(0x2724);
 			for (int i = 0; i < 90; i++) {


Commit: 4f7d766b4a61c1aa314bffe6c9d7e4c59a94c688
    https://github.com/scummvm/scummvm/commit/4f7d766b4a61c1aa314bffe6c9d7e4c59a94c688
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:55+01:00

Commit Message:
FREESCAPE: fixed a few bugs in the driller c64 music emulation

Changed paths:
    engines/freescape/games/driller/c64.music.cpp
    engines/freescape/games/driller/c64.music.h


diff --git a/engines/freescape/games/driller/c64.music.cpp b/engines/freescape/games/driller/c64.music.cpp
index 357f4a05af8..52493ae7a0b 100644
--- a/engines/freescape/games/driller/c64.music.cpp
+++ b/engines/freescape/games/driller/c64.music.cpp
@@ -99,7 +99,7 @@ const int NUM_INSTRUMENTS = sizeof(instrumentDataA0) / 8;
 
 // Arpeggio Data (0x157A - 0x157E)
 const uint8_t arpeggio_data[] = {0x00, 0x0C, 0x18};
-const int NUM_ARPEGGIOS = 1; // Only one arpeggio table is defined
+// Only one arpeggio table is defined (3 entries: +0, +12, +24 semitones)
 
 // Music Data Pointers and Structures
 // Need to load the actual PRG file into a buffer (_musicData)
@@ -168,9 +168,8 @@ const int NUM_PATTERNS = sizeof(pattern_addresses) / sizeof(pattern_addresses[0]
 // Tune Data (0x1054, 0x15D5 - 0x15E5)
 const uint8_t tune_tempo_data[] = {0x00, 0x03, 0x03}; // tempos for tune 0, 1, 2
 const uint8_t *const tune_track_data[][3] = {
-	{nullptr, nullptr, nullptr},                               // Tune 0 (no data specified, likely silent/unused)
+	{nullptr, nullptr, nullptr},                               // Tune 0 (null pointers = stop)
 	{voice1_track_data, voice2_track_data, voice3_track_data}, // Tune 1
-	{voice1_track_data, voice2_track_data, voice3_track_data}  // Tune 2 (Assume same as tune 1 for now if needed)
 };
 const int NUM_TUNES = sizeof(tune_tempo_data) / sizeof(tune_tempo_data[0]);
 
@@ -183,9 +182,8 @@ const int voice_sid_offset[] = {0, 7, 14};
 DrillerSIDPlayer::DrillerSIDPlayer() : _sid(nullptr),
 														  _playState(STOPPED),
 														  _targetTuneIndex(0),
-														  _globalTempo(3),       // Default tempo
-														  _globalTempoCounter(1), // Start immediately
-														  _framePhase(0)
+														  _globalTempo(3),        // Default tempo
+														  _globalTempoCounter(1)  // Start immediately
 {
 	initSID();
 
@@ -290,22 +288,14 @@ void DrillerSIDPlayer::onTimer() {
 	}
 
 	// Corresponds to voice_done (0x09A1)
-	// The original code increments x by 7 for each voice, so the third voice call has x=14 (0x0E)
-	// This check ensures the tempo counter is handled only once after all voices are processed.
-	// cpx #$0E ; bne @done
-	_framePhase += 7;
-	if (_framePhase >= 14) { // In practice, this will be 21, but we just need to check it's the third voice's turn
-		_framePhase = 0;
-
-		// dec tempo_ctr (0x09A5)
-		_globalTempoCounter--;
-
-		// bpl @done (0x09A8)
-		if (_globalTempoCounter < 0) {
-			// lda tempo; sta tempo_ctr (0x09AA)
-			_globalTempoCounter = _globalTempo;
-			debug(DEBUG_LEVEL >= 2, "Driller: Tempo Tick! Reloading counter to %d", _globalTempoCounter);
-		}
+	// After all 3 voices processed (cpx #$0E), handle tempo counter once per frame
+	// dec tempo_ctr (0x09A5)
+	_globalTempoCounter--;
+
+	// bpl @done (0x09A8)
+	if (_globalTempoCounter < 0) {
+		// lda tempo; sta tempo_ctr (0x09AA)
+		_globalTempoCounter = _globalTempo;
 	}
 }
 
@@ -464,13 +454,7 @@ void DrillerSIDPlayer::playVoice(int voiceIndex) {
 	const uint8_t *instA0 = &instrumentDataA0[instBase];
 	const uint8_t *instA1 = &instrumentDataA1[instBase];
 
-	// Hard Restart / Buzz Effect Check (Inst A0[7] & 0x01) - Apply if active
-	// This check was previously in applyNote, moved here to match L1005 check location relative to effects
-	if (v.hardRestartActive) {
-		applyHardRestart(v, sidOffset, instA0, instA1);
-	}
-
-	// Glide down effect? (L094E) - Inst A0[7] & 0x04
+	// Waveform transition effect (L0944-L095E) - Inst A0[7] & 0x04
 	// This logic updates ctrl register $D404, likely wave or gate
 	if (instA0[7] & 0x04) {
 		if (v.glideDownTimer > 0) { // voice1_two_ctr,x (0xD3E)
@@ -550,8 +534,8 @@ void DrillerSIDPlayer::playVoice(int voiceIndex) {
 			}
 
 			// Reset state related to previous note/effects for gate control
-			_tempControl3 = 0xFF; // Reset gate mask (0x09D0) - Currently unused in C++ code
-			v.whatever0 = 0;      // Reset effect states (0x09D5 onwards)
+			v.gateMask = 0xFF; // Reset gate mask (0x09D0: lda #$FF; sta control3)
+			v.whatever0 = 0;   // Reset effect states (0x09D5 onwards)
 			v.whatever1 = 0;
 			v.whatever2 = 0;
 
@@ -611,23 +595,17 @@ void DrillerSIDPlayer::playVoice(int voiceIndex) {
 					}
 					uint8_t portaParam = v.patternDataPtr[v.patternIndex]; // Consume data byte
 
-					if (v.currentNote > 0) {
-						// Set porta type (1=Down(FB), 2=Up(FC)) or (3=DownH, 4=UpH)
-						if (cmd == 0xFB) {                            // effect_fb_1
-							v.whatever2 = (instA0[7] & 0x02) ? 3 : 1; // (0A01)
-							debug(DEBUG_LEVEL >= 2, "Driller V%d: Cmd FB, Porta Down Param = $%02X (Type %d)", voiceIndex, portaParam, v.whatever2);
-						} else {                                      // FC (effect_fc_2)
-							v.whatever2 = (instA0[7] & 0x02) ? 4 : 2; // (0A17 -> 0A01)
-							debug(DEBUG_LEVEL >= 2, "Driller V%d: Cmd FC, Porta Up Param = $%02X (Type %d)", voiceIndex, portaParam, v.whatever2);
-						}
-
-						v.portaStepRaw = portaParam; // Store raw porta speed (0A0A / 0A19->0A0A)
-						v.whatever0 = 0;             // Reset vibrato state (0A0D)
-						v.whatever1 = 0;             // Reset arpeggio state (0A0F)
-						v.portaSpeed = 0;            // Force recalc
-					} else {
-						debug(DEBUG_LEVEL >= 2, "Driller V%d: Ignoring FB/FC command, no note playing.", voiceIndex);
+					// FB = porta type 1 (down lo), FC = porta type 2 (up lo)
+					// Assembly: FB -> lda #$01 (0x09FF), FC -> lda #$02 (0x0A17)
+					if (cmd == 0xFB) {
+						v.whatever2 = 1; // (0x09FF: lda #$01; sta whatever+2)
+					} else { // FC
+						v.whatever2 = 2; // (0x0A17: lda #$02)
 					}
+					v.portaStepRaw = portaParam; // sta voice1_something (0x0A0A)
+					v.whatever1 = 0;             // sta whatever+1 (0x0A0F)
+					v.whatever0 = 0;             // sta whatever (0x0A12)
+					v.portaSpeed = 0;            // Force recalc
 					v.patternIndex++; // Continue reading pattern (0A15)
 
 				} else if (cmd == 0xFA) { // --- Effect FA: Set Instrument --- (0x0A1B)
@@ -684,168 +662,160 @@ void DrillerSIDPlayer::playVoice(int voiceIndex) {
 					v.patternIndex++;
 
 				} else {                 // --- Plain Note --- (0x0A1D -> 0A44)
-					v.currentNote = cmd; // Store note value (0A44)
-					debug(DEBUG_LEVEL >= 2, "Driller V%d: Note Cmd = $%02X (%d)", voiceIndex, v.currentNote, v.currentNote);
-					// Set delay counter based on previously read duration (FD command)
-					v.delayCounter = v.noteDuration; // (0A47 -> 0A4A)
+					v.currentNote = cmd; // Store note value (0A44: sta stuff+3)
+					// Set delay counter based on previously read duration (0A47-0A4A)
+					v.delayCounter = v.noteDuration;
 
-					// Reset hard restart counters (0A4D)
+					// Reset hard restart counters (0A4D-0A52)
 					v.whatever3 = 0;
 					v.whatever4 = 0;
 
-					// Reset glide down timer (0A55)
+					// Reset glide down timer (0A55-0A57)
 					v.glideDownTimer = 2; // voice1_two_ctr = 2
 
-					// Handle legato/slide (Instrument A0[7] & 0x02) (0A5D)
-					if (instA0[7] & 0x02) { // Check legato bit
-						debug(DEBUG_LEVEL >= 3, "Driller V%d: Legato instrument flag set.", voiceIndex);
-					}
-
-					// Apply Note Data
+					// Apply Note Data (0A5D-0AB3)
 					applyNote(v, sidOffset, instA0, instA1, voiceIndex);
 
-					// Continue reading pattern (but we are done with this note)
+					// Continue reading pattern
 					v.patternIndex++;
-					noteProcessed = true; // Exit the pattern reading loop for this frame
+					noteProcessed = true;
 				}
 
 			} // End while(!noteProcessed)
 			// --- End of inlined pattern reading logic ---
+
+			// L0AFC: Post-note effect setup - determine which continuous effect is active
+			postNoteEffectSetup(v, sidOffset, instA0, instA1);
 		}
 	}
 
-	// ALWAYS apply continuous effects for the current state of the voice, then return.
+	// ALWAYS apply continuous effects (L0B33+) for the current state of the voice.
+	// This runs every frame: on tempo ticks after note processing, and on non-tempo ticks directly.
 	applyContinuousEffects(v, sidOffset, instA0, instA1);
-
-	// After processing note or commands for this tick, if a note wasn't fully processed (e.g. pattern end)
-	// we might need to apply effects. But if noteProcessed = true, applyNote was called which handles final writes.
-	// If noteProcessed = false (e.g. loop break), effects might need applying.
-	// Let's assume effects are only applied when a note holds or on non-tempo ticks.
-	// The call to applyContinuousEffects happens *outside* this loop if the delay counter held.
 }
 
 // --- Note Application ---
-// --- Note Application ---
+// Corresponds to @plain_note (0x0A44) through L0AAD (0x0AB3)
 void DrillerSIDPlayer::applyNote(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1, int voiceIndex) {
-	// Corresponds to 0xA70 onwards
-
-	uint8_t note = v.currentNote;
-	uint16_t newPulseWidth = 0;
-	uint8_t pwLoByte = 0;
-	uint8_t pwHiNibble = 0;
-	bool isRest = (note == 0);
-	uint8_t writeAD = 0;
-	uint8_t writeSR = 0;
-	int currentInstNum = 0;
-
-	// --- EFFECT INITIALIZATION ---
-	// This block correctly determines which continuous effect (Arp, Vib, etc.)
-	// should be active for the new note based on the instrument data.
-	v.whatever0 = 0; // Vibrato flag
-	v.whatever1 = 0; // Arpeggio flag
-	v.whatever2 = 0; // Portamento flag
-	v.portaSpeed = 0;
-
-	if (instA1[4] != 0) { // Arpeggio from InstA1[4]
-		uint8_t arpData = instA1[4];
-		v.arpTableIndex = arpData & 0x0F;
-		v.arpSpeedHiNibble = (arpData & 0xF0) >> 4;
-		if (v.arpTableIndex >= NUM_ARPEGGIOS) v.arpTableIndex = 0;
-		v.stuff_arp_counter = 0;
-		v.stuff_arp_note_index = 0;
-		v.whatever1 = 1;
-	} else if (instA1[0] != 0) { // Vibrato from InstA1[0]
-		v.things_vib_depth = instA1[0];
-		v.things_vib_delay_reload = instA1[1];
-		v.things_vib_delay_ctr = v.things_vib_delay_reload;
-		v.things_vib_state = 0;
-		v.whatever0 = 1;
-	} else if (instA0[5] != 0) { // Arpeggio from InstA0[5]
-		uint8_t arpData = instA0[5];
-		v.arpTableIndex = arpData & 0x0F;
-		v.arpSpeedHiNibble = (arpData & 0xF0) >> 4;
-		if (v.arpTableIndex >= NUM_ARPEGGIOS) v.arpTableIndex = 0;
-		v.stuff_arp_counter = 0;
-		v.stuff_arp_note_index = 0;
-		v.whatever1 = 1;
+	uint8_t note = v.currentNote; // Already stored at @plain_note (0x0A44)
+	// v.delayCounter already set from v.noteDuration (0x0A47-0x0A4A)
+	// v.whatever3/4 already reset to 0 (0x0A4D-0x0A52)
+	// v.glideDownTimer already set to 2 (0x0A55-0x0A57)
+
+	// Check legato bit instA0[7] & 0x02 (0x0A5D-0x0A6D)
+	if (instA0[7] & 0x02) {
+		// Legato: restore PW values from FA command backup
+		v.something_else[0] = v.something_else[1]; // something_else+1 -> something_else+0
+		v.something_else[2] = v.ctrl0;              // ctrl0 -> something_else+2
 	}
 
-	// --- NOTE HANDLING ---
-	if (isRest) {
+	// Handle note=0 (rest) at L0A70
+	if (note == 0) {
+		// Load previous note from things+6 (0x0A75)
 		note = v.currentNoteSlideTarget;
-		v.currentNoteSlideTarget = 0;
-		if (note == 0) {
-			v.keyOn = false;
-			goto WriteFinalControlReg;
-		}
-		v.keyOn = true; // Keep gate on for slide
+		v.currentNote = note; // Update stuff+3 equivalent
+		v.currentNoteSlideTarget = 0; // Clear things+6 (0x0A7B-0x0A7D)
+
+		// dec control3 (0x0A83)
+		v.gateMask--;
+
+		// bne L0AAD - since control3 was 0xFF, now 0xFE, always branches
+		// Skip frequency write entirely for rests, jump to L0AAD
 	} else {
-		v.currentNoteSlideTarget = note;
-		v.keyOn = true;
+		// Non-zero note: store as previous note (L0A88)
+		v.currentNoteSlideTarget = note; // things+6 = note
+
+		// Set frequency from note (L0A8C-L0AA1)
+		if (note >= 96) note = 95;
+		SID_Write(sidOffset + 1, frq_hi[note]); // $D401
+		SID_Write(sidOffset + 0, frq_lo[note]); // $D400
+		// Store in stuff variables: stuff[0]=lo, stuff[1]=lo, stuff[2]=hi, stuff[4]=hi
+		uint8_t fLo = frq_lo[note];
+		uint8_t fHi = frq_hi[note];
+		v.stuff_freq_porta_vib = fLo | (fHi << 8); // stuff[0]/[4]
+		v.stuff_freq_base = fLo | (fHi << 8);       // stuff[1]/[2]
+		v.stuff_freq_hard_restart = fLo | (fHi << 8);
+		v.currentFreq = v.stuff_freq_porta_vib;
+
+		// Write initial waveform (gate-on transient): instA0[6] -> $D404 (L0AA7-L0AAA)
+		SID_Write(sidOffset + 4, instA0[6]);
 	}
 
-	// --- FREQUENCY ---
-	if (note >= 96) note = 95;
-	v.baseFreq = frq_lo[note] | (frq_hi[note] << 8);
-	v.stuff_freq_base = v.baseFreq;
-	v.stuff_freq_porta_vib = v.baseFreq;
-	v.stuff_freq_hard_restart = v.baseFreq;
-	v.currentFreq = v.baseFreq;
-	SID_Write(sidOffset + 0, frq_lo[note]);
-	SID_Write(sidOffset + 1, frq_hi[note]);
+	// L0AAD: Write control register with gate mask
+	// lda instA0[1]; AND control3; sta $D404 (0x0AAD-0x0AB3)
+	SID_Write(sidOffset + 4, instA0[1] & v.gateMask);
 
-	// --- WAVEFORM ---
-	v.waveform = instA0[6];
+	// Write ADSR from instrument (0x0AB6-0x0ABF)
+	SID_Write(sidOffset + 5, instA0[2]); // Attack / Decay
+	SID_Write(sidOffset + 6, instA0[3]); // Sustain / Release
 
-	// --- HARD RESTART ---
-	if (instA0[7] & 0x01) {
-		v.hardRestartActive = true;
-		v.hardRestartDelay = 0;
-		v.hardRestartCounter = 0;
-		v.hardRestartValue = instA1[5];
-	} else {
-		v.hardRestartActive = false;
+	// Write Pulse Width from something_else (0x0AC2-0x0ACB)
+	SID_Write(sidOffset + 2, v.something_else[0]); // PW Lo
+	SID_Write(sidOffset + 3, v.something_else[2]); // PW Hi
+
+	v.pulseWidth = v.something_else[0] | (v.something_else[2] << 8);
+}
+
+// --- Post-Note Effect Setup ---
+// Corresponds to L0AFC in the assembly. Runs after each note is applied.
+// Determines which continuous effect should be active based on instrument data.
+void DrillerSIDPlayer::postNoteEffectSetup(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1) {
+	// L0AFC: lda voice1_things+6,x; beq L0B33
+	if (v.currentNoteSlideTarget == 0)
+		return; // No note stored, skip to continuous effects (PW LFO etc.)
+
+	// Check if portamento already active from FB/FC pattern command (L0B04)
+	// lda voice1_whatever+2,x; bne L0B17
+	if (v.whatever2 != 0) {
+		// Already have porta from pattern command, go to porta processing
+		return; // Porta will run in applyContinuousEffects
 	}
 
-	// --- ADSR (Corrected) ---
-	// As per disassembly at 0xAB6, ADSR is read from instA0[2] and instA0[3].
-	writeAD = instA0[2];
-	writeSR = instA0[3];
-	currentInstNum = v.instrumentIndex / 8;
-
-	// --- PATCH: Override ADSR for specific instruments ---
-	// Instruments 1 and 4 seem to have incorrect ADSR data in the disassembly,
-	// requiring this hardcoded override to sound correct.
-	if (currentInstNum == 1 || currentInstNum == 4) {
-		writeAD = 0xA0;
-		writeSR = 0xF0;
+	int instBase = v.instrumentIndex;
+	if (instBase < 0 || (size_t)instBase >= sizeof(instrumentDataA0))
+		instBase = 0;
+
+	// Check instA1[4] for instrument-level portamento (L0B09)
+	// lda possibly_instrument_a1+4,y; beq L0B1A
+	if (instA1[4] != 0) {
+		v.whatever2 = instA1[4];       // Store as porta type (L0B0E)
+		v.portaStepRaw = instA1[3];    // Store porta speed (L0B11-L0B14)
+		v.portaSpeed = 0;              // Force recalc
+		return; // jmp L0C5A - porta will run in applyContinuousEffects
 	}
 
-	SID_Write(sidOffset + 5, writeAD); // Attack / Decay
-	SID_Write(sidOffset + 6, writeSR); // Sustain / Release
-	debug(DEBUG_LEVEL >= 3, "Driller V%d: Set ADSR = $%02X / $%02X", voiceIndex, writeAD, writeSR);
-
-	// --- PULSE WIDTH (Corrected) ---
-	// As per disassembly at 0xAC2, Pulse Width is derived from the nibbles of instA0[0],
-	// which are loaded into the `something_else` variables by the FA (Set Instrument) command.
-	pwLoByte = v.something_else[0];
-	pwHiNibble = v.something_else[2] & 0x0F;
-	newPulseWidth = pwLoByte | (pwHiNibble << 8);
-	v.pulseWidth = newPulseWidth;
-	SID_Write(sidOffset + 2, v.pulseWidth & 0xFF);
-	SID_Write(sidOffset + 3, (v.pulseWidth >> 8) & 0x0F);
-	debug(DEBUG_LEVEL >= 3, "Driller V%d: Set PW = %d ($%03X) from instA0[0] nibbles", voiceIndex, v.pulseWidth, v.pulseWidth);
-
-WriteFinalControlReg:
-	// --- FINAL CONTROL REGISTER (GATE/WAVEFORM) ---
-	uint8_t ctrl = v.waveform;
-	if (v.keyOn) {
-		ctrl |= 0x01; // Gate On
-	} else {
-		ctrl &= 0xFE; // Gate Off
+	// Check instA0[5] for arpeggio (L0B1A -> L0E67)
+	// lda possibly_instrument_a0+5,y; beq L0B22
+	if (instA0[5] != 0) {
+		// L0E67: arpeggio setup
+		uint8_t arpData = instA0[5];
+		v.arpTableIndex = arpData & 0x0F;                    // and #$0F -> ctrl1
+		v.arpSpeedHiNibble = (arpData & 0xF0) >> 4;         // and #$F0; lsr*4 -> stuff+5
+		v.stuff_arp_counter = 0;                             // sta stuff+6
+		v.whatever1 = 1;                                     // sta whatever+1
+		v.whatever0 = 0;                                     // sta whatever
+		return; // jmp voice_done
+	}
+
+	// L0B22: Clear arpeggio flag (A=0 from beq)
+	v.whatever1 = 0;
+
+	// Check instA1[0] for vibrato (L0B25 -> L0E89)
+	// lda possibly_instrument_a1,y; beq L0B2D
+	if (instA1[0] != 0) {
+		// L0E89: vibrato setup
+		v.things_vib_depth = instA1[0];                      // sta things+1
+		v.things_vib_delay_reload = instA1[1];               // sta things+2
+		v.things_vib_delay_ctr = v.things_vib_delay_reload;  // sta things+3
+		v.things_vib_state = 0;                              // sta things
+		v.whatever1 = 0;                                     // sta whatever+1
+		v.whatever0 = 1;                                     // sta whatever
+		return; // jmp voice_done
 	}
-	SID_Write(sidOffset + 4, ctrl);
-	debug(DEBUG_LEVEL >= 2, "Driller V%d: Final Control Reg Write = $%02X (Wave=$%02X, Gate=%d)", voiceIndex, ctrl, v.waveform, v.keyOn);
+
+	// L0B2D: Clear vibrato flag (A=0 from beq)
+	v.whatever0 = 0;
+	// jmp voice_done
 }
 
 // --- Continuous Effect Application (Vibrato, Porta, Arp) ---
@@ -855,155 +825,150 @@ void DrillerSIDPlayer::applyContinuousEffects(VoiceState &v, int sidOffset, cons
 	uint16_t freq = v.stuff_freq_porta_vib; // Start with base freq + porta/vib from previous step
 	bool freqDirty = false;                 // Track if frequency needs writing
 
-	// Instrument A0[4] based frequency LFO (L0B33) - PW LFO?
+	// PW LFO (L0B33-L0B82) - instA0[4] = modulation speed (stored in control1)
 	uint8_t lfoSpeed = instA0[4];
 	if (lfoSpeed != 0) {
-		// This LFO modifies 'something_else', which we mapped to PW registers based on FA command logic?
-		// Or does it modify PW directly based on current PW? Let's assume it modifies current PW.
-		uint16_t currentPW = v.pulseWidth;   // Use the state variable
-		if (v.whatever2_vibDirToggle == 0) { // Direction toggle (0B3B)
-			currentPW += lfoSpeed;
-			if (currentPW > 0x0E00 || currentPW < lfoSpeed) { // Check wrap around too
-				currentPW = 0x0E00;                           // Clamp
-				v.whatever2_vibDirToggle = 1;                 // Change direction (0B5D)
+		// Operates on something_else[0] (lo byte) and something_else[2] (hi nibble)
+		if (v.whatever2_vibDirToggle == 0) {
+			// Add phase (L0B40-L0B5D): clc; adc control1 on lo; adc #0 on hi
+			uint16_t sum = (uint16_t)v.something_else[0] + lfoSpeed;
+			v.something_else[0] = sum & 0xFF;
+			v.something_else[2] = v.something_else[2] + (sum >> 8);
+			SID_Write(sidOffset + 2, v.something_else[0]); // $D402
+			SID_Write(sidOffset + 3, v.something_else[2]); // $D403
+			// clc; cmp #$0E - check hi byte >= 0x0E (L0B59)
+			if (v.something_else[2] >= 0x0E) {
+				v.whatever2_vibDirToggle = 1; // inc whatever2 (L0B5D)
 			}
 		} else {
-			// Need signed arithmetic potentially if currentPW could go below lfoSpeed
-			if (currentPW >= lfoSpeed) {
-				currentPW -= lfoSpeed;
-			} else {
-				currentPW = 0;
-			}
-			if (currentPW < 0x0800) {         // Limit check (0B7B)
-				currentPW = 0x0800;           // Clamp
-				v.whatever2_vibDirToggle = 0; // Change direction (0B7F)
+			// Subtract phase (L0B62-L0B7F): sec; sbc control1 on lo; sbc #0 on hi
+			uint16_t diff = (uint16_t)v.something_else[0] + 0x100 - lfoSpeed;
+			v.something_else[0] = diff & 0xFF;
+			if (diff < 0x100) // borrow
+				v.something_else[2]--;
+			SID_Write(sidOffset + 2, v.something_else[0]); // $D402
+			SID_Write(sidOffset + 3, v.something_else[2]); // $D403
+			// clc; cmp #$08 - check hi byte < 0x08 (L0B7B)
+			if (v.something_else[2] < 0x08) {
+				v.whatever2_vibDirToggle = 0; // dec whatever2 (L0B7F)
 			}
 		}
-		currentPW &= 0x0FFF;
-		if (v.pulseWidth != currentPW) {
-			v.pulseWidth = currentPW;
-			SID_Write(sidOffset + 2, v.pulseWidth & 0xFF);        // Write PW Lo (0B4A / 0B6C)
-			SID_Write(sidOffset + 3, (v.pulseWidth >> 8) & 0x0F); // Write PW Hi (0B55 / 0B77)
-			debug(1, "Driller 1: PW LFO Updated PW = %d ($%03X)", v.pulseWidth, v.pulseWidth);
-		}
+		v.pulseWidth = v.something_else[0] | (v.something_else[2] << 8);
 	}
 
 	// Arpeggio (L0B82) - Check 'whatever1' flag
 	if (v.whatever1) {
-		const uint8_t *arpTable = &arpeggio_data[0]; // Only one table defined
-
-		// Speed calculation from 0B98 - checks counter against 'stuff+5' (arpSpeedHiNibble)
-		uint8_t speed = v.arpSpeedHiNibble; // This was set from InstA1[4] or InstA0[5] hi nibble
-		if (speed == 0)
-			speed = 1; // Avoid division by zero or infinite loop
-
-		v.stuff_arp_counter++;
-		if (v.stuff_arp_counter >= speed) {
-			v.stuff_arp_counter = 0;
-			// Advance arpeggio note index (0BA0 / 0BBA)
-			v.stuff_arp_note_index = (v.stuff_arp_note_index + 1) % 3; // Cycle 0, 1, 2
-			debug(1, "Driller 1: Arp Step -> Note Index %d", v.stuff_arp_note_index);
+		// Assembly: single counter (stuff+6) cycles 0..speed-1, used directly as arp table index
+		// lda stuff+6; cmp stuff+5; bne L0BA5; lda #0; sta stuff+6
+		uint8_t speed = v.arpSpeedHiNibble; // stuff+5: set from instA0[5] hi nibble
+		if (v.stuff_arp_counter == speed) {
+			v.stuff_arp_counter = 0; // Reset when counter == speed (L0BA0)
 		}
 
-		// Calculate arpeggio note (0BA6)
-		uint8_t baseNote = v.currentNote; // Note from pattern
-		if (baseNote > 0 && baseNote < 96) {
-			uint8_t arpOffset = arpTable[v.stuff_arp_note_index]; // Offset from table (0BAA)
+		// tay; lda stuff+3; clc; adc arpeggio_0,y (L0BA5-L0BAD)
+		uint8_t baseNote = v.currentNote;
+		if (baseNote > 0 && baseNote < 96 && v.stuff_arp_counter < 3) {
+			uint8_t arpOffset = arpeggio_data[v.stuff_arp_counter]; // Counter IS the table index
 			uint8_t arpNote = baseNote + arpOffset;
 			if (arpNote >= 96)
-				arpNote = 95; // Clamp
+				arpNote = 95;
 
-			// Set frequency based on arpeggio note
 			freq = frq_lo[arpNote] | (frq_hi[arpNote] << 8);
-			freqDirty = true;
-			// Arpeggio overrides other frequency effects for this frame
-			goto WriteFrequency;
-		} else {
-			// If base note is invalid (e.g., 0), maybe use baseFreq? Or just skip arp?
-			// Fall through to allow other effects if arp base note is invalid
+			SID_Write(sidOffset + 0, frq_lo[arpNote]); // sta $D400 (L0BB1)
+			SID_Write(sidOffset + 1, frq_hi[arpNote]); // sta $D401 (L0BB7)
+			v.currentFreq = freq;
 		}
+
+		v.stuff_arp_counter++; // inc stuff+6 (L0BBA)
+		// jmp voice_done - arpeggio skips vibrato/porta
+		return;
 	}
 
 	// Vibrato (L0BC0 / L0BC8) - Check 'whatever0' flag
+	// Assembly applies frequency modification EVERY frame.
+	// The timer (things+3) only controls when the direction state advances.
 	if (v.whatever0) {
-		if (v.things_vib_delay_reload > 0) { // Only run if delay is set
-
-			// --- Fix 3a: Simplify Counter Logic ---
-			v.things_vib_delay_ctr--;          // Decrement first
-			if (v.things_vib_delay_ctr == 0) { // Check if zero AFTER decrementing
-											   // --- End Fix 3a ---
-
-				v.things_vib_delay_ctr = v.things_vib_delay_reload; // Reload counter
-
-				int state = v.things_vib_state;
-				int32_t current_freq_signed = v.stuff_freq_porta_vib; // Apply vibrato based on current freq (inc. porta)
-
-				// Use level 1 for this crucial debug message
-				debug(1, "Driller V1: Vib Step - State %d, Depth %d", state, (int16_t)v.things_vib_depth);
+		int state = v.things_vib_state;
+		uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF;
+		uint8_t freqHi = (v.stuff_freq_porta_vib >> 8) & 0xFF;
+
+		// Apply depth based on state (L0C06, L0C2F, L0BD1)
+		// States 0, 3, 4: subtract (down). States 1, 2: add (up).
+		// State 0: L0C06 (beq). States 1,2: L0C2F (cmp #3; bcc). States 3,4: fall through.
+		if (state == 1 || state == 2) {
+			// Add (L0C2F): clc; lda stuff,x; adc things+1,x
+			uint16_t sum = (uint16_t)freqLo + (v.things_vib_depth & 0xFF);
+			freqLo = sum & 0xFF;
+			freqHi = freqHi + (sum >> 8);
+		} else {
+			// Subtract (L0C06/L0BD1): sec; lda stuff,x; sbc things+1,x
+			uint16_t diff = (uint16_t)freqLo + 0x100 - (v.things_vib_depth & 0xFF);
+			freqLo = diff & 0xFF;
+			if (diff < 0x100) // borrow occurred
+				freqHi--;
+		}
 
-				// Apply depth based on state (L0C06, L0C2F, L0BD1)
-				// ... (rest of vibrato logic is likely okay) ...
-				// States 0, 2, 3 are down; State 1, 4 are up
-				if (state == 1 || state == 4) { // Up sweep
-					current_freq_signed += v.things_vib_depth;
-				} else { // Down sweep (0, 2, 3)
-					current_freq_signed -= v.things_vib_depth;
-				}
+		v.stuff_freq_porta_vib = (uint16_t)freqLo | ((uint16_t)freqHi << 8);
+		freq = v.stuff_freq_porta_vib;
+		freqDirty = true;
 
-				// Clamp frequency after modification
-				if (current_freq_signed < 0)
-					current_freq_signed = 0;
-				if (current_freq_signed > 0xFFFF)
-					current_freq_signed = 0xFFFF;
-				v.stuff_freq_porta_vib = (uint16_t)current_freq_signed; // Store result for next frame's base
-				freq = v.stuff_freq_porta_vib;                          // Use vibrato-modified frequency for this frame
-				freqDirty = true;
-
-				// Advance state (0BF4 / 0C29 / 0C52)
-				v.things_vib_state++;
-				if (v.things_vib_state >= 5) { // Cycle states 0..4 (0BFA)
-					v.things_vib_state = 1;    // Loop back to state 1 (upward sweep) (0BFE) - Correct based on diss.
-				}
-				// Use level 1 for this crucial debug message
-				debug(1, "Driller V1: Vib Freq Updated = %d, Next State %d", freq, v.things_vib_state);
+		// Decrement timer, advance state only when expired (dec things+3; bne done)
+		v.things_vib_delay_ctr--;
+		if (v.things_vib_delay_ctr == 0) {
+			v.things_vib_delay_ctr = v.things_vib_delay_reload;
+			v.things_vib_state++;
+			if (v.things_vib_state >= 5) { // cmp #$05; bcc
+				v.things_vib_state = 1;    // Reset to state 1 (0BFE)
 			}
 		}
 	} // end if(v.whatever0)
 
 	// Portamento (L0C5A) - Check 'whatever2' flag
-	if (v.whatever2) { // Note: 'else if' removed, allow porta+vib? Keep 'else if'.
-		// Calculate porta speed if not already done (or if param changed?)
-		if (v.portaSpeed == 0) {            // Calculate only once per porta command
-			int16_t speed = v.portaStepRaw; // Raw value from FB/FC command (e.g., 0x01 or 0x80)
-			// Disassembly L0C7B (type 1) / L0CA6 (type 2) / L0C96 (type 3) / L0C6B (type 4)
-			// Types 1 & 3 are down, 2 & 4 are up. Speed seems absolute value?
-			// Let's assume portaStepRaw is the step magnitude.
-			if (v.whatever2 == 1 || v.whatever2 == 3) { // Down
-				v.portaSpeed = -speed;                  // Ensure negative for down
-			} else {                                    // Up (2 or 4)
-				v.portaSpeed = speed;                   // Ensure positive for up
-			}
-			debug(1, "Driller 1: Porta Recalc Speed = %d (Raw=%d, Type=%d)", v.portaSpeed, v.portaStepRaw, v.whatever2);
+	// 4 distinct types matching assembly:
+	// Type 1 (L0C7B): CLC+SBC on lo byte (slide down, borrow to hi)
+	// Type 2 (L0CA6): CLC+ADC on lo byte (slide up, carry to hi)
+	// Type 3 (L0C96): SEC+SBC on hi byte only (fast slide down)
+	// Type >= 4 (L0C6B): CLC+ADC on hi byte only (fast slide up)
+	if (v.whatever2) {
+		uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF;       // stuff[0]
+		uint8_t freqHi = (v.stuff_freq_porta_vib >> 8) & 0xFF; // stuff[4]
+		uint8_t speed = v.portaStepRaw & 0xFF;                 // voice1_something
+
+		if (v.whatever2 == 1) {
+			// Type 1 (L0C7B): clc; sbc = subtract (speed+1) from lo, borrow to hi
+			uint16_t diff = (uint16_t)freqLo - speed; // CLC means borrow, so effectively -(speed+1)
+			freqLo = (diff - 1) & 0xFF; // CLC+SBC = subtract with extra borrow
+			if ((diff - 1) > 0xFF) freqHi--; // Propagate borrow
+			SID_Write(sidOffset + 0, freqLo);
+			SID_Write(sidOffset + 1, freqHi);
+		} else if (v.whatever2 == 2) {
+			// Type 2 (L0CA6): clc; adc on lo, carry to hi
+			uint16_t sum = (uint16_t)freqLo + speed;
+			freqLo = sum & 0xFF;
+			freqHi = freqHi + (sum >> 8);
+			SID_Write(sidOffset + 0, freqLo);
+			SID_Write(sidOffset + 1, freqHi);
+		} else if (v.whatever2 == 3) {
+			// Type 3 (L0C96): sec; sbc on hi byte only (fast slide down)
+			freqHi = freqHi - speed;
+			SID_Write(sidOffset + 1, freqHi);
+		} else {
+			// Type >= 4 (L0C6B): clc; adc on hi byte only (fast slide up)
+			freqHi = freqHi + speed;
+			SID_Write(sidOffset + 1, freqHi);
 		}
 
-		// Apply portamento step
-		int32_t tempFreqSigned = v.stuff_freq_porta_vib; // Apply to current frequency
-		tempFreqSigned += v.portaSpeed;                  // Add signed speed
-
-		// Clamp frequency
-		if (tempFreqSigned > 0xFFFF)
-			tempFreqSigned = 0xFFFF;
-		if (tempFreqSigned < 0)
-			tempFreqSigned = 0;
+		v.stuff_freq_porta_vib = (uint16_t)freqLo | ((uint16_t)freqHi << 8);
+		v.currentFreq = v.stuff_freq_porta_vib;
+	}
 
-		v.stuff_freq_porta_vib = (uint16_t)tempFreqSigned; // Store result for next frame
-		freq = v.stuff_freq_porta_vib;                     // Use the porta-modified frequency for this frame
-		freqDirty = true;
-		debug(DEBUG_LEVEL >= 3, "Driller: Porta Step -> Freq = %d", freq);
+	// After porta, check for hard restart (L0CBE)
+	// lda instA0[7]; and #$01; beq voice_done; jmp L1005
+	if (instA0[7] & 0x01) {
+		applyHardRestart(v, sidOffset, instA0, instA1);
 	}
 
-WriteFrequency:
-	// Write final frequency to SID if it was changed by effects
+	// Write final frequency if modified by vibrato (arp and porta write directly)
 	if (freqDirty && v.currentFreq != freq) {
 		v.currentFreq = freq;
 		SID_Write(sidOffset + 0, freq & 0xFF);
diff --git a/engines/freescape/games/driller/c64.music.h b/engines/freescape/games/driller/c64.music.h
index 0ecd3127b80..57168d0e2d1 100644
--- a/engines/freescape/games/driller/c64.music.h
+++ b/engines/freescape/games/driller/c64.music.h
@@ -207,16 +207,13 @@ class DrillerSIDPlayer {
 	uint8_t _targetTuneIndex; // Tune index requested via startMusic
 
 	// Global Timing
-	uint8_t _globalTempo;      // Tempo value for current tune (0xD10)
+	uint8_t _globalTempo;       // Tempo value for current tune (0xD10)
 	int8_t _globalTempoCounter; // Frame counter for tempo (0xD12), signed to handle < 0 check
-	uint8_t _framePhase;       // Tracks which voice is being processed (0, 7, 14)
 
 	// Voice States
 	VoiceState _voiceState[3];
 
-	// Internal helpers
-	uint8_t _tempControl3; // Temporary storage for gate mask (0xD13)
-	// uint8_t _tempControl1; // Temp storage from instrument data (0xD11)
+	// Gate mask is now per-voice (v.gateMask) matching assembly's control3
 
 public:
 	DrillerSIDPlayer();
@@ -232,6 +229,7 @@ private:
 	void handleResetVoices();
 	void playVoice(int voiceIndex);
 	void applyNote(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1, int voiceIndex);
+	void postNoteEffectSetup(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1);
 	void applyContinuousEffects(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1);
 	void applyHardRestart(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1);
 };


Commit: ba72f4991f613c86e389f08bdec3284b56ae3e37
    https://github.com/scummvm/scummvm/commit/ba72f4991f613c86e389f08bdec3284b56ae3e37
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:55+01:00

Commit Message:
FREESCAPE: add gate bitmap in castle cpc

Changed paths:
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/cpc.cpp


diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index e377951a8fd..31652e5133a 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -419,7 +419,7 @@ void CastleEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *inf
 void CastleEngine::beforeStarting() {
 	if (isDOS())
 		waitInLoop(250);
-	else if (isSpectrum())
+	else if (isSpectrum() || isCPC())
 		waitInLoop(100);
 	else if (isAmiga() || isAtariST())
 		waitInLoop(250);
@@ -1700,6 +1700,8 @@ void CastleEngine::drawLiftingGate(Graphics::Surface *surface) {
 		duration = 100;
 	else if (isAmiga() || isAtariST())
 		duration = 250;
+	else if (isCPC())
+		duration = 100;
 
 	if ((_gameStateControl == kFreescapeGameStateStart || _gameStateControl == kFreescapeGameStateRestart) && _ticks <= duration) { // Draw the _gameOverBackgroundFrame gate lifting up slowly
 		int gate_w = _gameOverBackgroundFrame->w;
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index f0e86d68a64..a451f469965 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -231,6 +231,125 @@ void CastleEngine::loadAssetsCPCFullGame() {
 	// Flag Animation: CPC offset 0x2654 (16x9 px, 4 frames)
 	_flagFrames = loadFramesWithHeaderCPC(&file, 0x2654, 4, cpcPalette);
 
+	// Gate image (portcullis) for game start/end animation.
+	// The CPC gate is NOT a pre-rendered bitmap; it is procedurally generated
+	// from small pixel pattern tables stored at file offset 0x75EF-0x764C.
+	// Structure: 10 columns × 6 bytes (=240 px wide), repeating bands of
+	// 4-row horizontal bars and 20-row inter-bar vertical bars, with a
+	// single last-row at the bottom. Total height = 119 px (CPC viewport).
+	{
+		// Horizontal bar pattern: 4 rows × 6 bytes (file offset 0x760B)
+		static const byte kGateHorizBar[4][6] = {
+			{0xD2, 0xF0, 0xF0, 0xF0, 0xF0, 0xB4},
+			{0xD2, 0xFA, 0xF8, 0xFB, 0xFB, 0xB5},
+			{0xDA, 0xFB, 0xFF, 0xF0, 0xFE, 0xB5},
+			{0x5A, 0xF0, 0xF0, 0xF0, 0xF0, 0xA5},
+		};
+		// Inter-bar pattern: 20 rows × 2 bytes (file offset 0x7623)
+		// Byte 0 = left-edge pixels (ink in 0xCC bit positions = pixels 0,1)
+		// Byte 1 = right-edge pixels (ink in 0x33 bit positions = pixels 2,3)
+		static const byte kGateInterBar[20][2] = {
+			{0xC0, 0x30}, {0xC0, 0x30}, {0xC8, 0x31}, {0xC8, 0x31},
+			{0xC0, 0x31}, {0xC8, 0x31}, {0xC0, 0x31}, {0xC0, 0x31},
+			{0xC0, 0x30}, {0xC0, 0x31}, {0xC0, 0x31}, {0xC8, 0x30},
+			{0xC8, 0x30}, {0xC0, 0x30}, {0xC8, 0x30}, {0xC8, 0x30},
+			{0xC8, 0x30}, {0xC8, 0x31}, {0xC8, 0x31}, {0xC8, 0x31},
+		};
+		// Last row pattern: 1 row × 2 bytes (file offset 0x764B)
+		static const byte kGateLastRow[2] = {0x80, 0x10};
+
+		static const int kGateWidth = 240;   // 10 columns × 6 bytes × 4 px/byte
+		static const int kGateHeight = 119;  // CPC max gate y-coordinate (0x77)
+		static const int kColumns = 10;
+		static const int kBytesPerCol = 6;
+
+		uint32 keyColor = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x24, 0xA5);
+
+		_gameOverBackgroundFrame = new Graphics::ManagedSurface();
+		_gameOverBackgroundFrame->create(kGateWidth, kGateHeight, _gfx->_texturePixelFormat);
+		_gameOverBackgroundFrame->fillRect(Common::Rect(0, 0, kGateWidth, kGateHeight), keyColor);
+
+		for (int y = 0; y < kGateHeight; y++) {
+			int fromBottom = kGateHeight - 1 - y;
+
+			if (fromBottom == 0) {
+				// Last row: edge pixels only (same masking as inter-bar)
+				for (int col = 0; col < kColumns; col++) {
+					int bx = col * kBytesPerCol * 4;
+					// Left edge byte: pixels 0,1 from pattern
+					for (int p = 0; p < 2; p++) {
+						int ink = getCPCPixel(kGateLastRow[0], p, true);
+						if (ink)
+							_gameOverBackgroundFrame->setPixel(bx + p, y, cpcPalette[ink]);
+					}
+					// Right edge byte: pixels 2,3 from pattern
+					for (int p = 2; p < 4; p++) {
+						int ink = getCPCPixel(kGateLastRow[1], p, true);
+						if (ink)
+							_gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, cpcPalette[ink]);
+					}
+				}
+			} else {
+				// Determine row type from bottom-up gate structure:
+				// fromBottom 1-14: bottom inter-bar (14 rows, pattern indices 0-13)
+				// fromBottom 15-18: bottom horizontal bar (4 rows)
+				// fromBottom >= 19: repeating 24-row sections (20 inter-bar + 4 horiz-bar)
+				bool isHorizBar = false;
+				int patIdx = 0;
+
+				if (fromBottom <= 14) {
+					// Bottom inter-bar section
+					patIdx = 14 - fromBottom; // 0-13
+				} else if (fromBottom <= 18) {
+					// Bottom horizontal bar section
+					isHorizBar = true;
+					patIdx = 18 - fromBottom; // 0-3
+				} else {
+					// Repeating zone: 24-row cycle (20 inter-bar + 4 horizontal bar)
+					int inSection = (fromBottom - 19) % 24;
+					if (inSection < 20) {
+						patIdx = 19 - inSection; // 0-19
+					} else {
+						isHorizBar = true;
+						patIdx = 23 - inSection; // 0-3
+					}
+				}
+
+				if (isHorizBar) {
+					// Horizontal bar: all 6 bytes per column are gate pixels
+					for (int col = 0; col < kColumns; col++) {
+						int bx = col * kBytesPerCol * 4;
+						for (int bi = 0; bi < kBytesPerCol; bi++) {
+							byte cpcByte = kGateHorizBar[patIdx][bi];
+							for (int p = 0; p < 4; p++) {
+								int ink = getCPCPixel(cpcByte, p, true);
+								if (ink)
+									_gameOverBackgroundFrame->setPixel(bx + bi * 4 + p, y, cpcPalette[ink]);
+							}
+						}
+					}
+				} else {
+					// Inter-bar: only edge pixels per column, middle is transparent
+					for (int col = 0; col < kColumns; col++) {
+						int bx = col * kBytesPerCol * 4;
+						// Left edge (byte 0): pixels 0,1 from pattern byte 0
+						for (int p = 0; p < 2; p++) {
+							int ink = getCPCPixel(kGateInterBar[patIdx][0], p, true);
+							if (ink)
+								_gameOverBackgroundFrame->setPixel(bx + p, y, cpcPalette[ink]);
+						}
+						// Right edge (byte 5): pixels 2,3 from pattern byte 1
+						for (int p = 2; p < 4; p++) {
+							int ink = getCPCPixel(kGateInterBar[patIdx][1], p, true);
+							if (ink)
+								_gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, cpcPalette[ink]);
+						}
+					}
+				}
+			}
+		}
+	}
+
 	// Note: Riddle frames are not loaded from external files for CPC.
 	// The _riddleTopFrame, _riddleBackgroundFrame, and _riddleBottomFrame
 	// will remain nullptr (initialized in constructor).
@@ -261,6 +380,9 @@ void CastleEngine::loadAssetsCPCFullGame() {
 }
 
 void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
+	drawLiftingGate(surface);
+	drawDroppingGate(surface);
+
 	uint32 color = _gfx->_paperColor;
 	uint8 r, g, b;
 


Commit: 49f2dc9b34b8c4a73ee4bac959ba3aab622c656d
    https://github.com/scummvm/scummvm/commit/49f2dc9b34b8c4a73ee4bac959ba3aab622c656d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:55+01:00

Commit Message:
FREESCAPE: properly parse and show dark amiga title

Changed paths:
    engines/freescape/games/dark/amiga.cpp


diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 9308d0c502c..738bd3e430a 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -20,6 +20,8 @@
  */
 #include "common/file.h"
 
+#include "graphics/palette.h"
+
 #include "freescape/freescape.h"
 #include "freescape/games/dark/dark.h"
 #include "freescape/language/8bitDetokeniser.h"
@@ -29,7 +31,39 @@ namespace Freescape {
 void DarkEngine::loadAssetsAmigaFullGame() {
 	Common::File file;
 	file.open("0.drk");
-	_title = loadAndConvertNeoImage(&file, 0x9930);
+	// Load title image: Amiga non-interleaved bitplanes with Atari ST palette
+	// Palette: 16 words at file offset 0x9934, Atari ST 3-bit $0RGB format
+	file.seek(0x9934);
+	Graphics::Palette pal(16);
+	for (int i = 0; i < 16; i++) {
+		byte v1 = file.readByte();
+		byte v2 = file.readByte();
+		byte r = floor((v1 & 0x07) * 255.0 / 7.0);
+		byte g = floor((v2 & 0x70) * 255.0 / 7.0 / 16.0);
+		byte b = floor((v2 & 0x07) * 255.0 / 7.0);
+		pal.set(i, r, g, b);
+	}
+
+	// Bitplanes: 4 planes x 8000 bytes at file offset 0x99B0, non-interleaved
+	file.seek(0x99B0);
+	Graphics::ManagedSurface *titleSurface = new Graphics::ManagedSurface();
+	titleSurface->create(320, 200, Graphics::PixelFormat::createFormatCLUT8());
+	titleSurface->fillRect(Common::Rect(0, 0, 320, 200), 0);
+	for (int plane = 0; plane < 4; plane++) {
+		for (int y = 0; y < 200; y++) {
+			for (int x = 0; x < 40; x++) {
+				byte b = file.readByte();
+				for (int n = 0; n < 8; n++) {
+					int px = x * 8 + (7 - n);
+					int bit = ((b >> n) & 0x01) << plane;
+					int sample = titleSurface->getPixel(px, y) | bit;
+					titleSurface->setPixel(px, y, sample);
+				}
+			}
+		}
+	}
+	titleSurface->convertToInPlace(_gfx->_texturePixelFormat, pal.data(), pal.size());
+	_title = titleSurface;
 	file.close();
 
 	Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.drk", "0.drk", 798);


Commit: b4868a69f73229fc260bc819b960fe6106b82bae
    https://github.com/scummvm/scummvm/commit/b4868a69f73229fc260bc819b960fe6106b82bae
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:32:56+01:00

Commit Message:
FREESCAPE: EMSCRIPTEN -> USE_FORCED_GLES

Changed paths:
    engines/freescape/gfx_opengl_shaders.cpp


diff --git a/engines/freescape/gfx_opengl_shaders.cpp b/engines/freescape/gfx_opengl_shaders.cpp
index 06ba3db09f1..404f500a6fb 100644
--- a/engines/freescape/gfx_opengl_shaders.cpp
+++ b/engines/freescape/gfx_opengl_shaders.cpp
@@ -575,12 +575,12 @@ void OpenGLShaderRenderer::renderFace(const Common::Array<Math::Vector3d> &verti
 	_triangleShader->use();
 	_triangleShader->setUniform("mvpMatrix", _mvpMatrix);
 
-	#if !defined(EMSCRIPTEN)
+#if !USE_FORCED_GLES2
 	if (_debugRenderWireframe) {
 		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
 		useColor(0, 255, 0);
 	}
-	#endif
+#endif
 
 	if (vertices.size() == 2) {
 		const Math::Vector3d &v1 = vertices[1];
@@ -598,10 +598,10 @@ void OpenGLShaderRenderer::renderFace(const Common::Array<Math::Vector3d> &verti
 		glDrawArrays(GL_LINES, 0, 2);
 
 		glLineWidth(1);
-		#if !defined(EMSCRIPTEN)
+#if !USE_FORCED_GLES2
 		if (_debugRenderWireframe)
 			glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
-		#endif
+#endif
 		return;
 	}
 
@@ -643,10 +643,10 @@ void OpenGLShaderRenderer::renderFace(const Common::Array<Math::Vector3d> &verti
 		glDrawArrays(GL_LINES, 0, 2);
 	}
 
-	#if !defined(EMSCRIPTEN)
+#if !USE_FORCED_GLES2
 	if (_debugRenderWireframe)
 		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
-	#endif
+#endif
 }
 
 void OpenGLShaderRenderer::depthTesting(bool enabled) {




More information about the Scummvm-git-logs mailing list