[Scummvm-git-logs] scummvm master -> 1dd797d863ab204a2a9f8330478d097b393b2f5a

sev- noreply at scummvm.org
Mon Jun 29 23:10:56 UTC 2026


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

Summary:
8d299a192b CHAMBER: Introduce isEgaLikeRenderer() for the shared EGA/Amiga path
68a54a742d CHAMBER: Add Amiga renderer, cursor and platform selection
76113959d7 CHAMBER: Load Amiga static data and per-language text files
16edb1aae2 CHAMBER: Compose the Amiga room palette from the zone palette_index
7a470db3e7 CHAMBER: Decode the Amiga sprite banks
5e9c9d0ffd CHAMBER: Fix Amiga script padding and timer-thread gauss race
b2cae9e832 CHAMBER: Minor Amiga review fixes
240eaeecab CHAMBER: Fix Amiga saucer animation and end screen
a5a88ba33d CHAMBER: Exit the end screen on input instead of hanging
d40e898b3c CHAMBER: Fix portrait animation pacing on modern hosts
c809bd0cb7 CHAMBER: Fix the Amiga title palette
350d2469f0 CHAMBER: Fix borders for the rectangular UI boxes
1dd797d863 CHAMBER: Fix warning message at character presentation


Commit: 8d299a192b5ab8e3750d53cd1859bd59db11806d
    https://github.com/scummvm/scummvm/commit/8d299a192b5ab8e3750d53cd1859bd59db11806d
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Introduce isEgaLikeRenderer() for the shared EGA/Amiga path

The Amiga port uses the same planar 16-colour, 1-byte-per-pixel chunky
graphics pipeline as EGA; only the palette, the static-data loader and a
few filenames differ. Add an isEgaLikeRenderer() helper and route the
graphics code through it instead of comparing against kRenderEGA, so the
upcoming Amiga renderer reuses the EGA sprite/portrait/transition paths
unchanged. No behavioural change for EGA or CGA.

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    engines/chamber/anim.cpp
    engines/chamber/chamber.h
    engines/chamber/dialog.cpp
    engines/chamber/kult.cpp
    engines/chamber/portrait.cpp
    engines/chamber/room.cpp
    engines/chamber/script.cpp


diff --git a/engines/chamber/anim.cpp b/engines/chamber/anim.cpp
index 18a8108a2e3..328919848da 100644
--- a/engines/chamber/anim.cpp
+++ b/engines/chamber/anim.cpp
@@ -60,7 +60,7 @@ void getScratchBuffer(byte mode) {
 	// otherwise a large lutin overruns its slot into the next one - and the top
 	// slot overruns the end of scratch_mem1 into the adjacent sprites_list[],
 	// corrupting it (later crashing in blitSpritesToBackBuffer/restoreImage).
-	uint16 slot = (g_vm->_videoMode == Common::kRenderEGA) ? 3200 : 1600;
+	uint16 slot = (isEgaLikeRenderer()) ? 3200 : 1600;
 	if (mode & 0x80)
 		offs += slot * 2;
 	if (mode & 0x40)
@@ -80,7 +80,7 @@ void animLoadSprite(byte **panim) {
 void clipSprite(byte *x, byte *y, byte *sprw, byte *sprh, byte **sprite, int8 dx, int8 dy) {
 	if (anim_flags == 7)
 		return;
-	uint16 bytes_per_col = (g_vm->_videoMode == Common::kRenderEGA) ? 4 : 2;
+	uint16 bytes_per_col = (isEgaLikeRenderer()) ? 4 : 2;
 	if (anim_flags & 4) {
 		if (anim_cycle == 0)
 			return;
@@ -115,7 +115,7 @@ void clipSprite(byte *x, byte *y, byte *sprw, byte *sprh, byte **sprite, int8 dx
 }
 
 void copyScreenBlockWithDotEffect(byte *source, byte x, byte y, byte width, byte height, byte *target) {
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		/* EGA: linear 1 byte/pixel. Reveal the block in the same scattered
 		   ("dot dissolve") order as the CGA path, blitting periodically so the
 		   transition is animated instead of an instant copy. */
@@ -252,7 +252,7 @@ void playAnimCore(byte **panim) {
 			sprw = *sprite++;
 			sprh = *sprite++;
 
-			if (g_vm->_videoMode == Common::kRenderEGA)
+			if (isEgaLikeRenderer())
 				pitch = sprw * 4;
 			else
 				pitch = sprw * 2;
diff --git a/engines/chamber/chamber.h b/engines/chamber/chamber.h
index b23bf427327..2e175cb8b76 100644
--- a/engines/chamber/chamber.h
+++ b/engines/chamber/chamber.h
@@ -108,6 +108,12 @@ void init(void);
 
 extern ChamberEngine *g_vm;
 
+// Amiga shares the EGA planar 16-colour pipeline; graphics code branches on this
+inline bool isEgaLikeRenderer() {
+	return g_vm->_videoMode == Common::kRenderEGA ||
+	       g_vm->_videoMode == Common::kRenderAmiga;
+}
+
 } // End of namespace Chamber
 
 #endif
diff --git a/engines/chamber/dialog.cpp b/engines/chamber/dialog.cpp
index 7b92359c6b8..f14efee9b27 100644
--- a/engines/chamber/dialog.cpp
+++ b/engines/chamber/dialog.cpp
@@ -193,7 +193,7 @@ void showPromptAnim(void) {
 }
 
 void promptWait(void) {
-	if (g_vm->_videoMode == Common::kRenderEGA)
+	if (isEgaLikeRenderer())
 		g_vm->_renderer->blitToScreen(0, 0, 320, 200);
 	cursor_anim_phase = 0;
 
diff --git a/engines/chamber/kult.cpp b/engines/chamber/kult.cpp
index 4f133c1c064..118823877d2 100644
--- a/engines/chamber/kult.cpp
+++ b/engines/chamber/kult.cpp
@@ -254,7 +254,8 @@ Common::Error ChamberEngine::init() {
 
 	// Initialize graphics using following:
 	bool isCustomHerc = false;
-	if (_videoMode == Common::RenderMode::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
+		// EGA and Amiga use the chunky 8bpp pipeline (1 byte/pixel, linear lines)
 		_screenW = 320;
 		_screenH = 200;
 		_screenBits = 8;
diff --git a/engines/chamber/portrait.cpp b/engines/chamber/portrait.cpp
index 1c956b95811..1dcc65b533c 100644
--- a/engines/chamber/portrait.cpp
+++ b/engines/chamber/portrait.cpp
@@ -328,7 +328,7 @@ void drawBoxAroundSpot(void) {
 	ofs = *(uint16 *)(buffer + 2);
 
 	/*decode ofs back to x:y*/
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		y = ofs / EGA_BYTES_PER_LINE;
 		x = ofs % EGA_BYTES_PER_LINE;
 		w *= 4; /* w was stored in CGA byte units */
@@ -451,7 +451,7 @@ void animPortrait(byte layer, byte index, byte delay) {
 		g_vm->_renderer->loadPortraitWithFrame(portrait - 1);
 		if (*ani == 0xFF) {
 			ani++;
-			if (g_vm->_videoMode == Common::kRenderEGA)
+			if (isEgaLikeRenderer())
 				ega_loadPortrait(&ani, ani + 3);
 			else
 				loadPortrait(&ani, ani + 3);
diff --git a/engines/chamber/room.cpp b/engines/chamber/room.cpp
index b2ce4ba5b91..014d92a675c 100644
--- a/engines/chamber/room.cpp
+++ b/engines/chamber/room.cpp
@@ -260,7 +260,7 @@ void selectSpotCursor(void) {
 				curs = CURSOR_CROSSHAIR;
 		}
 	}
-	if (g_vm->_videoMode == Common::kRenderEGA)
+	if (isEgaLikeRenderer())
 		cursor_shape = souri_data + curs * (CURSOR_WIDTH * CURSOR_HEIGHT / 4);
 	else
 		cursor_shape = souri_data + curs * CURSOR_WIDTH * CURSOR_HEIGHT * 2 / 4;
@@ -306,7 +306,7 @@ static const int16 background_draw_steps_hga[] = {
 Draw main backgound pattern, in spiral-like order
 */
 void drawBackground(byte *target, byte vblank) {
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		ega_drawBackground(target);
 		return;
 	}
@@ -465,7 +465,7 @@ void initRoomDoorInfo(byte index) {
 	aptr = doors_list[index - 1];
 	info->flipped = (aptr[1] & 0x80) ? ~0 : 0;
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		for (i = 0; i < kNumDoorSprites; i++) {
 			byte x, y, w, h, ox;
 			Graphics::Surface *surf = ega_puzzl_res->getSprite(aptr[0]);
@@ -547,7 +547,7 @@ Draw sliding door
 void drawRoomDoor(void) {
 	int16 i;
 	doorinfo_t *info = (doorinfo_t *)scratch_mem2;
-	bool isEGA = (g_vm->_videoMode == Common::kRenderEGA);
+	bool isEGA = (isEgaLikeRenderer());
 	for (i = 0; i < kNumDoorSprites; i++) {
 		byte w = info->layer[i].width;
 		byte h = info->layer[i].height;
@@ -583,7 +583,7 @@ void animRoomDoorOpen(byte index) {
 		drawRoomDoor();
 		waitVBlank();
 		info->layer[1].height -= 2;
-		if (g_vm->_videoMode == Common::kRenderEGA)
+		if (isEgaLikeRenderer())
 			info->layer[1].pixels += info->layer[1].width * 4 * 2;
 		else
 			info->layer[1].pixels += info->layer[1].width * 2 * 2;
@@ -609,7 +609,7 @@ void animRoomDoorClose(byte index) {
 	oldheight = info->layer[1].height;
 	oldpixels = info->layer[1].pixels;
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		info->layer[1].pixels += info->layer[1].width * 4 * (info->layer[1].height - 1);
 	} else {
 		info->layer[1].pixels += info->layer[1].width * 2 * (info->layer[1].height - 1);
@@ -622,7 +622,7 @@ void animRoomDoorClose(byte index) {
 		waitVBlank();
 #endif
 		info->layer[1].height += 2;
-		if (g_vm->_videoMode == Common::kRenderEGA)
+		if (isEgaLikeRenderer())
 			info->layer[1].pixels -= info->layer[1].width * 4 * 2;
 		else
 			info->layer[1].pixels -= info->layer[1].width * 2 * 2;
@@ -830,7 +830,7 @@ void drawPersons(void) {
 Draw room's static object to backbuffer
 */
 void drawRoomStaticObject(byte *aptr, byte *rx, byte *ry, byte *rw, byte *rh) {
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		Graphics::Surface *surf = ega_puzzl_res->getSprite(aptr[0]);
 		byte x = aptr[1];
 		byte y = aptr[2];
@@ -1078,7 +1078,7 @@ void refreshZone(void) {
 	// skip it (e.g. entering through an already-animated door). skip_zone_transition
 	// is set by SCR_42_LoadZone; plain zone changes (walking around The Ring / through
 	// passages via SCR_36_ChangeZone) leave it 0 and so get the dot-dissolve reveal.
-	if (!skip_zone_transition && g_vm->_videoMode == Common::kRenderEGA
+	if (!skip_zone_transition && isEgaLikeRenderer()
 	        && zoneHasWalkTransition(script_byte_vars.zone_area)) {
 		ega_zoneRevealWipe();
 	} else {
@@ -1157,7 +1157,7 @@ void loadLutinSprite(uint16 lutidx) {
 	lutW = *lutin_entry++; /* composite width in CGA bytes */
 	lutH = *lutin_entry++; /* composite height in pixels */
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		/*EGA: build CLUT8 flat buffer in lutin_mem: [0]=lutW, [1]=lutH, then lutW*4 bytes/row*/
 		uint16 pw = (uint16)lutW * 4;
 		lutin_mem[0] = lutW;
@@ -1260,7 +1260,7 @@ static void egaDrawLutinComposite(uint16 lutidx, byte *target, uint16 baseOfs) {
 Draw specific room's person idle sprite
 */
 void drawCharacterSprite(byte spridx, byte x, byte y, byte *target) {
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		egaDrawLutinComposite(spridx, target, g_vm->_renderer->calcXY_p(x, y));
 		return;
 	}
@@ -1287,7 +1287,7 @@ char drawZoneAniSprite(rect_t *rect, uint16 index, byte *target) {
 
 			zsprite_draw_ofs = g_vm->_renderer->calcXY_p(rect->sx, rect->sy);
 
-			if (g_vm->_videoMode == Common::kRenderEGA) {
+			if (isEgaLikeRenderer()) {
 				byte *entry, *entry_end;
 				entry = seekToEntry(lutin_data, spridx, &entry_end);
 				zsprite_w = entry[0]; /* CGA byte width */
@@ -1516,7 +1516,7 @@ void prepareTurkey(void) {
 Load puzzl sprite to scratch and init draw params
 */
 uint16 getPuzzlSprite(byte index, byte x, byte y, uint16 *w, uint16 *h, uint16 *ofs) {
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		Graphics::Surface *surf = ega_puzzl_res->getSprite(index);
 		*w = surf->w / 4;
 		*h = surf->h;
@@ -1603,7 +1603,7 @@ byte *loadMursmSprite(byte index) {
 	byte *pinfo, *end;
 	pinfo = seekToEntry(mursm_data, index, &end);
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		/* EGA: build a CLUT8 buffer (80 px wide × 59 rows = 4720 bytes) */
 		const uint16 egaPitch = 80; /* 20 CGA bytes × 4 px/byte */
 		memset(sprit_load_buffer, 0, egaPitch * 59);
@@ -1789,7 +1789,7 @@ void theWallPhase1_DoorClose1(void) {
 	loadZone();
 
 	spr = loadMursmSprite(0);
-	if (g_vm->_videoMode == Common::kRenderEGA)
+	if (isEgaLikeRenderer())
 		spr += cur_frame_width * 4 - 4;
 	else
 		spr += cur_frame_width - 1;
@@ -1816,19 +1816,19 @@ void theWallPhase2_DoorClose2(void) {
 	loadZone();
 
 	spr = loadMursmSprite(0);
-	if (g_vm->_videoMode == Common::kRenderEGA)
+	if (isEgaLikeRenderer())
 		spr += cur_frame_width * 4 - 4;
 	else
 		spr += cur_frame_width - 1;
 	cur_image_coords_x = 64 / 4;
-	if (g_vm->_videoMode == Common::kRenderEGA)
+	if (isEgaLikeRenderer())
 		g_vm->_renderer->animLiftToRight(10, spr - 40, cur_frame_width, 1 + 10, cur_image_size_h, frontbuffer, g_vm->_renderer->calcXY_p(cur_image_coords_x, cur_image_coords_y));
 	else
 		g_vm->_renderer->animLiftToRight(10, spr - 10, cur_frame_width, 1 + 10, cur_image_size_h, frontbuffer, g_vm->_renderer->calcXY_p(cur_image_coords_x, cur_image_coords_y));
 
 	spr = loadMursmSprite(1);
 	cur_image_coords_x = 220 / 4;
-	if (g_vm->_videoMode == Common::kRenderEGA)
+	if (isEgaLikeRenderer())
 		g_vm->_renderer->animLiftToLeft(10, spr, cur_frame_width, 1 + 10, cur_image_size_h, frontbuffer, g_vm->_renderer->calcXY_p(cur_image_coords_x, cur_image_coords_y) - 40);
 	else
 		g_vm->_renderer->animLiftToLeft(10, spr, cur_frame_width, 1 + 10, cur_image_size_h, frontbuffer, g_vm->_renderer->calcXY_p(cur_image_coords_x, cur_image_coords_y) - 10);
@@ -1845,7 +1845,7 @@ void drawTheWallDoors(void) {
 	switch (script_byte_vars.zone_index) {
 	case 9:
 	case 102:
-		if (g_vm->_videoMode == Common::kRenderEGA) {
+		if (isEgaLikeRenderer()) {
 			/* EGA buffer is 80 px wide; CGA +10 bytes = 40 EGA pixels */
 			g_vm->_renderer->blit(loadMursmSprite(0) + 40, 20, 10, 59, SCREENBUFFER, g_vm->_renderer->calcXY_p(64 / 4, 32));
 			if (g_vm->getLanguage() == Common::EN_USA) {
diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index e6f77016710..da855160a1d 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -669,7 +669,7 @@ uint16 SCR_5_DrawPortraitLiftRight(void) {
 		return 0;
 
 	/*TODO: use local args instead of globals*/
-	if (g_vm->_videoMode == Common::kRenderEGA)
+	if (isEgaLikeRenderer())
 		g_vm->_renderer->animLiftToRight(width, cur_image_pixels + width * 4 - 4, width, 1, height, SCREENBUFFER, g_vm->_renderer->calcXY_p(x, y));
 	else
 		g_vm->_renderer->animLiftToRight(width, cur_image_pixels + width - 1, width, 1, height, SCREENBUFFER, g_vm->_renderer->calcXY_p(x, y));
@@ -866,7 +866,7 @@ uint16 SCR_D_DrawPortraitDotEffect(void) {
 	if (!drawPortrait(&script_ptr, &x, &y, &width, &height))
 		return 0;
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		uint16 pw = width * 4;
 		cur_image_end = pw * height;
 		uint16 baseOfs = g_vm->_renderer->calcXY_p(x, y);
@@ -940,7 +940,7 @@ uint16 drawPortraitZoomed(byte **params) {
 	zwidth = *((*params)++);
 	zheight = *((*params)++);
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		/*EGA can't zoom: draw at original size; dirty rect already has correct dimensions from drawPortrait*/
 		g_vm->_renderer->zoomImage(cur_image_pixels, cur_image_size_w, cur_image_size_h, cur_image_size_w, cur_image_size_h, frontbuffer, cur_image_offs);
 	} else {
@@ -989,7 +989,7 @@ uint16 SCR_19_HidePortraitLiftLeft(void) {
 
 	/*TODO: This originally was done by reusing door sliding routine*/
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		offs += 4;
 		while (--width)
 			g_vm->_renderer->hideScreenBlockLiftToLeft(1, SCREENBUFFER, backbuffer, width, height, SCREENBUFFER, offs);
@@ -1050,7 +1050,7 @@ uint16 SCR_1A_HidePortraitLiftRight(void) {
 
 	/*TODO: This originally was done by reusing door sliding routine*/
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		offs = g_vm->_renderer->calcXY_p(x + width - 2, y);
 		while (--width)
 			g_vm->_renderer->hideScreenBlockLiftToRight(1, SCREENBUFFER, backbuffer, width, height, SCREENBUFFER, offs);
@@ -1108,7 +1108,7 @@ uint16 SCR_1B_HidePortraitLiftUp(void) {
 		return 0;
 	}
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		offs = g_vm->_renderer->calcXY_p(x, y + 1);
 		while (--height)
 			g_vm->_renderer->hideScreenBlockLiftToUp(1, SCREENBUFFER, backbuffer, width, height, SCREENBUFFER, offs);
@@ -1155,7 +1155,7 @@ uint16 SCR_1C_HidePortraitLiftDown(void) {
 		return 0;
 	}
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		offs = g_vm->_renderer->calcXY_p(x, y + height - 2);
 		while (--height)
 			g_vm->_renderer->hideScreenBlockLiftToDown(1, SCREENBUFFER, backbuffer, width, height, SCREENBUFFER, offs);
@@ -1421,7 +1421,7 @@ void drawStars(star_t *stars, int16 iter, byte *target) {
 		short z, x, y;
 		byte pixel, mask;
 
-		if (g_vm->_videoMode == Common::kRenderEGA)
+		if (isEgaLikeRenderer())
 			target[stars->ofs] = 0;
 		else
 			target[stars->ofs] &= stars->mask;
@@ -1445,7 +1445,7 @@ void drawStars(star_t *stars, int16 iter, byte *target) {
 			continue;
 		}
 
-		if (g_vm->_videoMode == Common::kRenderEGA) {
+		if (isEgaLikeRenderer()) {
 			stars->ofs = g_vm->_renderer->calcXY_p(x, y);
 			pixel = (stars->z < 0xE00) ? 15 : 8;
 			stars->pixel = pixel;
@@ -2898,7 +2898,7 @@ static void AnimSaucer(void) {
 	  saucer region, so leftover sprites from the final game frame (player,
 	  HUD pixels) would otherwise survive on the black backdrop right through to
 	  THE END screen.*/
-	if (g_vm->_videoMode == Common::kRenderEGA)
+	if (isEgaLikeRenderer())
 		memset(frontbuffer, 0, sizeof(SCREENBUFFER));
 	g_vm->_renderer->backBufferToRealFull();
 	g_vm->_renderer->colorSelect(0x30);
@@ -3554,7 +3554,7 @@ void DrawStickyNet(void) {
 
 	/*16x30 is the net sprite size*/
 
-	if (g_vm->_videoMode == Common::kRenderEGA) {
+	if (isEgaLikeRenderer()) {
 		Graphics::Surface *surf = ega_puzzl_res->getSprite(80);
 		uint16 sprW = surf->w;
 		uint16 sprH = surf->h;


Commit: 68a54a742d0f36aa1cb49514b88a20be73f43a5b
    https://github.com/scummvm/scummvm/commit/68a54a742d0f36aa1cb49514b88a20be73f43a5b
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Add Amiga renderer, cursor and platform selection

Add the AmigaRenderer (custom 12-bit palette over the same planar
16-colour graphics as EGA) and select it by platform. Implement its
cursor decoder (SOURI.BIN hardware sprites) so the renderer's vtable is
complete, and wire up the Amiga title screen and fade-in ramp in the
boot path. The renderer reuses the EGA chunky pipeline; only the palette
differs.

Assisted-by: Claude:claude-opus-4.8

Changed paths:
  A engines/chamber/amiga.cpp
  A engines/chamber/amiga.h
    engines/chamber/chamber.cpp
    engines/chamber/cursor.cpp
    engines/chamber/kult.cpp
    engines/chamber/module.mk


diff --git a/engines/chamber/amiga.cpp b/engines/chamber/amiga.cpp
new file mode 100644
index 00000000000..a9cd9dfca1d
--- /dev/null
+++ b/engines/chamber/amiga.cpp
@@ -0,0 +1,191 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/file.h"
+#include "common/system.h"
+#include "graphics/paletteman.h"
+
+#include "chamber/chamber.h"
+#include "chamber/common.h"
+#include "chamber/amiga.h"
+#include "chamber/resdata.h"
+
+namespace Chamber {
+
+const byte *amiga_palette_table = nullptr;
+const byte *amiga_room_palette_table = nullptr;
+
+// Static resources are sliced straight out of the EU KULT executable
+struct AmigaResEnt {
+	const char *name;
+	byte **buffer;
+	uint32 offset;
+	uint32 size;
+};
+
+#define AMIGA_PAL_OFFSET 122836   // title fade ramp, end of LUTIN
+
+// Each room takes a shared base and drops its own 5-word delta over slots 1..5
+#define AMIGA_ROOM_PAL_OFFSET 40268
+#define AMIGA_ROOM_DELTA_OFFSET 40068
+#define AMIGA_NUM_ROOM_PALETTES 20
+
+static const byte *amiga_room_delta_table = nullptr;
+
+// Accent colours every room shares. Slots 1..5 get overwritten by the delta
+static const uint16 amiga_room_base_pal[16] = {
+	0x000, // 0  black (background)
+	0x000, // 1  stone  } slots 1..5 overwritten by the per-room delta
+	0x000, // 2  stone
+	0x000, // 3  stone
+	0x000, // 4  stone
+	0x000, // 5  stone
+	0x101, // 6  crevice
+	0x100, // 7  dark detail
+	0x886, // 8  stone/sprite highlight
+	0x64A, // 9  blue-purple accent
+	0x700, // 10 dark red
+	0xEA8, // 11 peach (dialog boxes)
+	0xD66, // 12 red (medallion accent)
+	0x446, // 13 slate blue (portrait background)
+	0xA8B, // 14 light mauve
+	0xFFF  // 15 white (text box / highlights)
+};
+
+static AmigaResEnt amiga_res[] = {
+	{ "SOUCO.BIN", &souco_data,  37724,   424 },
+	{ "ZONES.BIN", &zones_data,  43250,  9014 },
+	{ "TEMPL.BIN", &templ_data,  52318, 27336 },
+	{ "MURSM.BIN", &mursm_data,  79654,    76 },
+	{ "ANIMA.BIN", &anima_data,  79730,  2046 },
+	{ "ANICO.BIN", &anico_data,  81776,   667 },
+	{ "ARPLA.BIN", &arpla_data, 105612,  8024 },
+	{ "CARAC.BIN", &carpc_data, 113636,   384 },
+	{ "GAUSS.BIN", &gauss_data, 114148,  2880 },
+	{ "ALEAT.BIN", &aleat_data, 117028,   256 },
+	{ "ICONE.BIN", &icone_data, 117284,  2752 },
+	{ "LUTIN.BIN", &lutin_data, 120036,  2800 },
+};
+static const int kAmigaNumRes = sizeof(amiga_res) / sizeof(amiga_res[0]);
+
+int16 loadAmigaStaticData() {
+	// Idempotent: called before the title splash and again from loadStaticData()
+	if (amiga_palette_table != nullptr)
+		return 1;
+
+	Common::File kult;
+	if (!kult.open("KULT")) {
+		warning("loadAmigaStaticData(): cannot open KULT");
+		return 0;
+	}
+
+	uint32 sz = kult.size();
+	byte *raw = new byte[sz];
+	if (kult.read(raw, sz) != sz) {
+		warning("loadAmigaStaticData(): short read on KULT");
+		delete[] raw;
+		return 0;
+	}
+
+	// Reuse _pxiData (freed by the destructor) to own the KULT image
+	delete[] g_vm->_pxiData;
+	g_vm->_pxiData = raw;
+
+	for (int i = 0; i < kAmigaNumRes; i++) {
+		if (amiga_res[i].offset + amiga_res[i].size > sz) {
+			warning("loadAmigaStaticData(): %s past EOF", amiga_res[i].name);
+			return 0;
+		}
+		*amiga_res[i].buffer = raw + amiga_res[i].offset;
+	}
+
+	amiga_palette_table = raw + AMIGA_PAL_OFFSET;
+	amiga_room_palette_table = raw + AMIGA_ROOM_PAL_OFFSET;
+	amiga_room_delta_table = raw + AMIGA_ROOM_DELTA_OFFSET;
+
+	// Amiga glyphs sit one bit too high, shift them back into the low nibble
+	for (uint32 i = 0; i < 384; i++)
+		carpc_data[i] = (carpc_data[i] >> 1) & 0x0F;
+
+	// SOURI (cursor) is a separate file, not embedded in KULT
+	static byte souri_buf[RES_SOURI_MAX];
+	if (!loadFile("SOURI.BIN", souri_buf))
+		warning("loadAmigaStaticData(): SOURI.BIN not found");
+	souri_data = souri_buf;
+
+	return 1;
+}
+
+void amigaApplyPalette(byte index) {
+	if (!amiga_palette_table)
+		return;
+
+	// Out-of-range indexes (e.g. CGA colorSelect bytes) are a no-op
+	if (index >= AMIGA_NUM_PALETTES)
+		return;
+
+	const byte *src = amiga_palette_table + index * (16 * 2);
+	byte palData[16 * 3];
+	for (int c = 0; c < 16; c++) {
+		uint16 w = (src[c * 2] << 8) | src[c * 2 + 1];   // big-endian 0x0RGB
+		// Expand each 4-bit channel to 8 bits
+		palData[c * 3 + 0] = ((w >> 8) & 0xF) * 17;
+		palData[c * 3 + 1] = ((w >> 4) & 0xF) * 17;
+		palData[c * 3 + 2] = ( w       & 0xF) * 17;
+	}
+	g_system->getPaletteManager()->setPalette(palData, 0, 16);
+}
+
+void amigaApplyRoomPalette(byte index) {
+	if (!amiga_room_delta_table)
+		return;
+	if (index >= AMIGA_NUM_ROOM_PALETTES)
+		index = 0;
+
+	// Base palette plus this room's 5-word delta overlaid onto slots 1..5
+	uint16 pal[16];
+	for (int c = 0; c < 16; c++)
+		pal[c] = amiga_room_base_pal[c];
+	const byte *delta = amiga_room_delta_table + index * (5 * 2);
+	for (int c = 0; c < 5; c++)
+		pal[1 + c] = (delta[c * 2] << 8) | delta[c * 2 + 1];
+
+	byte palData[16 * 3];
+	for (int c = 0; c < 16; c++) {
+		uint16 w = pal[c];
+		palData[c * 3 + 0] = ((w >> 8) & 0xF) * 17;
+		palData[c * 3 + 1] = ((w >> 4) & 0xF) * 17;
+		palData[c * 3 + 2] = ( w       & 0xF) * 17;
+	}
+	g_system->getPaletteManager()->setPalette(palData, 0, 16);
+}
+
+void AmigaRenderer::switchToGraphicsMode() {
+	// No-op until loadAmigaStaticData() has run
+	amigaApplyPalette(0);
+}
+
+void AmigaRenderer::colorSelect(byte csel) {
+	// On Amiga this carries the real palette index (see room.cpp)
+	amigaApplyPalette(csel);
+}
+
+} // End of namespace Chamber
diff --git a/engines/chamber/amiga.h b/engines/chamber/amiga.h
new file mode 100644
index 00000000000..0687e807a51
--- /dev/null
+++ b/engines/chamber/amiga.h
@@ -0,0 +1,54 @@
+/* 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 CHAMBER_AMIGA_H
+#define CHAMBER_AMIGA_H
+
+#include "chamber/renderer.h"
+
+namespace Chamber {
+
+class AmigaRenderer : public EGARenderer {
+public:
+	void switchToGraphicsMode() override;
+	void colorSelect(byte csel) override;     // csel is the real palette index
+	void selectCursor(uint16 num) override;   // SOURI.BIN: big-endian cursor planes
+};
+
+#define AMIGA_NUM_PALETTES 31
+
+// 31x16 12-bit RGB title palette table inside the loaded KULT image
+extern const byte *amiga_palette_table;
+
+// Master's-Orbit full 16-colour palette (the one scene not using base+delta)
+extern const byte *amiga_room_palette_table;
+
+void amigaApplyPalette(byte index);
+
+// Compose a room palette from the zone palette_index (base + per-index delta)
+void amigaApplyRoomPalette(byte index);
+
+// Slice the static resources out of the uncompressed KULT executable; returns 1 on success
+int16 loadAmigaStaticData();
+
+} // End of namespace Chamber
+
+#endif // CHAMBER_AMIGA_H
diff --git a/engines/chamber/chamber.cpp b/engines/chamber/chamber.cpp
index a0d0277eaf1..6e189bc3aa9 100644
--- a/engines/chamber/chamber.cpp
+++ b/engines/chamber/chamber.cpp
@@ -38,6 +38,7 @@
 #include "chamber/chamber.h"
 #include "chamber/detection.h"
 #include "chamber/renderer.h"
+#include "chamber/amiga.h"
 #include "chamber/script.h"
 #include "chamber/resdata.h"
 #include "chamber/room.h"
@@ -77,10 +78,16 @@ ChamberEngine::ChamberEngine(OSystem *syst, const ADGameDescription *desc)
 	if (_renderMode == Common::kRenderHercA)
 		_videoMode = Common::kRenderHercG;
 
+	// Amiga has its own renderer, picked by platform
+	if (_gameDescription->platform == Common::kPlatformAmiga)
+		_videoMode = Common::kRenderAmiga;
+
 	_screenH = _screenW = _screenBits = _screenBPL = _screenPPB = 0;
 	_line_offset = _line_offset2 = _fontHeight = _fontWidth = 0;
 
-	if (_videoMode == Common::kRenderEGA)
+	if (_videoMode == Common::kRenderAmiga)
+		_renderer = new AmigaRenderer();
+	else if (_videoMode == Common::kRenderEGA)
 		_renderer = new EGARenderer();
 	else
 		_renderer = new CGARenderer();
diff --git a/engines/chamber/cursor.cpp b/engines/chamber/cursor.cpp
index 38d0e6cd84b..1a97d6247ff 100644
--- a/engines/chamber/cursor.cpp
+++ b/engines/chamber/cursor.cpp
@@ -27,6 +27,7 @@
 #include "chamber/resdata.h"
 #include "chamber/cga.h"
 #include "chamber/ega.h"
+#include "chamber/amiga.h"
 #include "chamber/renderer.h"
 #include "graphics/cursorman.h"
 #include "graphics/palette.h"
@@ -134,6 +135,51 @@ void EGARenderer::selectCursor(uint16 num) {
 	CursorMan.showMouse(true);
 }
 
+void AmigaRenderer::selectCursor(uint16 num) {
+	cursor_x_shift = cursor_shifts[num][0];
+	cursor_y_shift = cursor_shifts[num][1];
+
+	byte *dst = cursorImage;
+
+	// 72-byte hardware sprite: skip the 4-byte header, then 16 rows of two BE words
+	// plane0 is the outline, plane1 the body
+	const int kAmigaCursorStride = 72;
+	const int kAmigaCursorHeader = 4;
+	cursor_shape = souri_data + num * kAmigaCursorStride + kAmigaCursorHeader;
+	byte *src = cursor_shape;
+	// white reticle, red on a hotspot; the rest use yellow or white
+	bool reticle = (num == CURSOR_TARGET || num == CURSOR_CROSSHAIR);
+	byte mainColor = 0;                                                      /*plane0 only: black edges*/
+	byte bodyColor = reticle ? ((cursor_color == 0xAA) ? 12 : 15)
+	                         : ((cursor_color == 0xAA) ? 14 : 15);           /*plane0+plane1: interior*/
+	for (int16 y = 0; y < CURSOR_HEIGHT; y++) {
+		uint16 plane0 = ((uint16)src[0] << 8) | (uint16)src[1];
+		uint16 plane1 = ((uint16)src[2] << 8) | (uint16)src[3];
+		src += 4;
+		for (int16 x = 0; x < CURSOR_WIDTH; x++) {
+			byte bit0 = (plane0 >> (CURSOR_WIDTH - 1 - x)) & 1;
+			byte bit1 = (plane1 >> (CURSOR_WIDTH - 1 - x)) & 1;
+			if (!bit0 && !bit1)
+				*dst++ = 255; /*transparent*/
+			else if (bit1)
+				*dst++ = bodyColor;
+			else
+				*dst++ = mainColor;
+		}
+	}
+
+	CursorMan.replaceCursor(cursorImage, CURSOR_WIDTH, CURSOR_HEIGHT, cursor_x_shift, cursor_y_shift, 255);
+	// Fixed palette so the cursor stays visible: 0 black, 12 red, 14 yellow, 15 white
+	static const byte cursorPal[16 * 3] = {
+		0, 0, 0,        0, 0, 0,  0, 0, 0,  0, 0, 0,
+		0, 0, 0,        0, 0, 0,  0, 0, 0,  0, 0, 0,
+		0, 0, 0,        0, 0, 0,  0, 0, 0,  0, 0, 0,
+		238, 17, 68,    0, 0, 0,  255, 255, 0, 255, 255, 255
+	};
+	CursorMan.replaceCursorPalette(cursorPal, 0, 16);
+	CursorMan.showMouse(true);
+}
+
 /*
 Build cursor sprite for its current pixel-grained position
 */
diff --git a/engines/chamber/kult.cpp b/engines/chamber/kult.cpp
index 118823877d2..e3e03e2dd36 100644
--- a/engines/chamber/kult.cpp
+++ b/engines/chamber/kult.cpp
@@ -30,6 +30,7 @@
 #include "chamber/decompr.h"
 #include "chamber/cga.h"
 #include "chamber/ega.h"
+#include "chamber/amiga.h"
 #include "chamber/ega_resource.h"
 #include "chamber/anim.h"
 #include "chamber/cursor.h"
@@ -300,7 +301,18 @@ Common::Error ChamberEngine::init() {
 
 	Graphics::Surface *splash = nullptr;
 
-	if (_videoMode == Common::RenderMode::kRenderEGA) {
+	if (_videoMode == Common::kRenderAmiga) {
+		// Amiga title: static resources (incl. palette) live in KULT, load them first
+		loadAmigaStaticData();
+		splash = ega_loadFond("PRES.BIN");
+		if (!splash) {
+			_shouldQuit = true;
+			return Common::kNoError;
+		}
+		// Last palette index is the full-brightness end of the title fade ramp
+		g_vm->_renderer->colorSelect(AMIGA_NUM_PALETTES - 1);
+		g_vm->_renderer->backBufferToRealFull();
+	} else if (_videoMode == Common::RenderMode::kRenderEGA) {
 		/* EGA title screen */
 		splash = ega_loadFond("PRESEGA.EGA");
 		if (!splash) {
@@ -355,7 +367,9 @@ Common::Error ChamberEngine::init() {
 	}
 
 	if (splash) {
-		if (g_vm->_videoMode != Common::RenderMode::kRenderEGA)
+		// EGA/Amiga splashes wrap ega_backbuffer; only CGA/Herc own their pixels
+		if (g_vm->_videoMode != Common::RenderMode::kRenderEGA &&
+		    g_vm->_videoMode != Common::kRenderAmiga)
 			splash->free();
 		delete splash;
 	}
diff --git a/engines/chamber/module.mk b/engines/chamber/module.mk
index 3f388b106a4..f7ec62e37e5 100644
--- a/engines/chamber/module.mk
+++ b/engines/chamber/module.mk
@@ -1,6 +1,7 @@
 MODULE := engines/chamber
 
 MODULE_OBJS := \
+	amiga.o \
 	anim.o \
 	bkbuff.o \
 	cga.o \


Commit: 76113959d737f04a21962269c84516166ee6f59c
    https://github.com/scummvm/scummvm/commit/76113959d737f04a21962269c84516166ee6f59c
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Load Amiga static data and per-language text files

The Amiga build keeps the static resources uncompressed inside the KULT
executable instead of a compressed PXI module, and ships the per-language
VERB/MOTS/DESC/DIAL files (D/E/F suffix) rather than the generic ...I.BIN
names. Add loadAmigaStaticData() and route resource loading accordingly.

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    engines/chamber/resdata.cpp


diff --git a/engines/chamber/resdata.cpp b/engines/chamber/resdata.cpp
index e9ddf45a3f8..05e1e373d70 100644
--- a/engines/chamber/resdata.cpp
+++ b/engines/chamber/resdata.cpp
@@ -27,6 +27,7 @@
 #include "chamber/resdata.h"
 #include "chamber/decompr.h"
 #include "chamber/ega.h"
+#include "chamber/amiga.h"
 
 namespace Chamber {
 
@@ -155,6 +156,10 @@ Load resident data files. Original game has all these data files embedded in the
 NB! Static data includes the font file, don't use any text print routines before it's loaded.
 */
 int16 loadStaticData() {
+	// Amiga keeps the static resources uncompressed inside the KULT executable
+	if (g_vm->_videoMode == Common::kRenderAmiga)
+		return loadAmigaStaticData();
+
 	Common::File pxi;
 
 	if (g_vm->_videoMode == Common::kRenderEGA)
@@ -261,16 +266,28 @@ ResEntry_t res_texts[] = {
 	{"$", NULL}
 };
 
+// Amiga ships per-language text files (D/E/F suffix); English set for now
+ResEntry_t res_texts_amiga[] = {
+	{"VERBE.BIN", vepci_data},
+	{"MOTSE.BIN", motsi_data},
+	{"$", NULL}
+};
+
 /*
 Load strings data (commands/names)
 */
 int16 loadVepciData() {
+	if (g_vm->_videoMode == Common::kRenderAmiga)
+		return loadFilesList(res_texts_amiga);
 	return loadFilesList(res_texts);
 }
 
 Graphics::Surface *loadFond(void) {
 	if (g_vm->_videoMode == Common::kRenderEGA)
 		return ega_loadFond("FOND.EGA");
+	// Amiga FOND.BIN is the same planar format as EGA
+	if (g_vm->_videoMode == Common::kRenderAmiga)
+		return ega_loadFond("FOND.BIN");
 	return loadSplash("FOND.BIN");
 }
 
@@ -302,10 +319,17 @@ ResEntry_t res_desci[] = {
 	{"$", NULL}
 };
 
+ResEntry_t res_desci_amiga[] = {
+	{"DESCE.BIN", desci_data},
+	{"$", NULL}
+};
+
 /*
 Load strings data (obj. descriptions)
 */
 int16 loadDesciData(void) {
+	if (g_vm->_videoMode == Common::kRenderAmiga)
+		return loadFilesList(res_desci_amiga);
 	while (!loadFilesList(res_desci))
 		askDisk2();
 	return 1;
@@ -316,10 +340,17 @@ ResEntry_t res_diali[] = {
 	{"$", NULL}
 };
 
+ResEntry_t res_diali_amiga[] = {
+	{"DIALE.BIN", diali_data},
+	{"$", NULL}
+};
+
 /*
 Load strings data (dialogs)
 */
 int16 loadDialiData(void) {
+	if (g_vm->_videoMode == Common::kRenderAmiga)
+		return loadFilesList(res_diali_amiga);
 	while (!loadFilesList(res_diali))
 		askDisk2();
 	return 1;


Commit: 16edb1aae2ca24351220ff0eb1ef9adeffe11b2b
    https://github.com/scummvm/scummvm/commit/16edb1aae2ca24351220ff0eb1ef9adeffe11b2b
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Compose the Amiga room palette from the zone palette_index

Apply the zone palette as soon as the zone loads so intro and special
screens (which may bypass refreshZone) are not left on the previous
title palette.

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    engines/chamber/room.cpp


diff --git a/engines/chamber/room.cpp b/engines/chamber/room.cpp
index 014d92a675c..e44bbfdc8f1 100644
--- a/engines/chamber/room.cpp
+++ b/engines/chamber/room.cpp
@@ -27,6 +27,7 @@
 #include "chamber/cga.h"
 #include "chamber/ega.h"
 #include "chamber/ega_resource.h"
+#include "chamber/amiga.h"
 #include "chamber/print.h"
 #include "chamber/anim.h"
 #include "chamber/cursor.h"
@@ -158,12 +159,19 @@ static const byte cga_color_sels[] = {
 };
 
 void selectSpecificPalette(byte index) {
-	g_vm->_renderer->colorSelect(cga_color_sels[index]);
+	// Amiga composes the room palette from the zone palette_index
+	if (g_vm->_videoMode == Common::kRenderAmiga)
+		amigaApplyRoomPalette(index);
+	else
+		g_vm->_renderer->colorSelect(cga_color_sels[index]);
 }
 
 
 void selectPalette(void) {
-	g_vm->_renderer->colorSelect(cga_color_sels[script_byte_vars.palette_index]);
+	if (g_vm->_videoMode == Common::kRenderAmiga)
+		amigaApplyRoomPalette(script_byte_vars.palette_index);
+	else
+		g_vm->_renderer->colorSelect(cga_color_sels[script_byte_vars.palette_index]);
 }
 
 /*
@@ -392,6 +400,10 @@ void loadZone(void) {
 	next_turkey_cmd = 0;
 	next_vorts_cmd = 0;
 	script_byte_vars.used_commands = 0;
+
+	// Apply the zone palette at load time for screens that skip refreshZone()
+	if (g_vm->_videoMode == Common::kRenderAmiga)
+		selectPalette();
 }
 
 void resetZone(void) {


Commit: 7a470db3e76e29b3d6eeb2a8361c52698c8e015a
    https://github.com/scummvm/scummvm/commit/7a470db3e76e29b3d6eeb2a8361c52698c8e015a
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Decode the Amiga sprite banks

The Amiga sprite banks share the EGA record layout but store the record
size big-endian and swap the width/height byte pair; the pixels are
word-planar (one big-endian word per 4-pixel column). Add
appendFromStreamAmiga() to decode them, a shared placeholder for missing
sprites, and load the merged Amiga banks (SPRIT/PUZZL/A/B).

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    engines/chamber/ega_resource.cpp
    engines/chamber/ega_resource.h
    engines/chamber/kult.cpp


diff --git a/engines/chamber/ega_resource.cpp b/engines/chamber/ega_resource.cpp
index 59a95266e76..32b9f71b94b 100644
--- a/engines/chamber/ega_resource.cpp
+++ b/engines/chamber/ega_resource.cpp
@@ -46,6 +46,19 @@ EgaSpriteResource::~EgaSpriteResource() {
 	}
 }
 
+Graphics::Surface *EgaSpriteResource::getSprite(uint index) const {
+	if (index < _sprites.size())
+		return _sprites[index];
+
+	// Shared 1x1 placeholder for missing sprites (keeps blits in-bounds)
+	static Graphics::Surface *dummy = nullptr;
+	if (!dummy) {
+		dummy = new Graphics::Surface();
+		dummy->create(1, 1, Graphics::PixelFormat::createFormatCLUT8());
+	}
+	return dummy;
+}
+
 void EgaSpriteResource::appendFromFile(const char *filename) {
 	Common::File fd;
 	if (!fd.open(filename))
@@ -53,6 +66,64 @@ void EgaSpriteResource::appendFromFile(const char *filename) {
 	appendFromStream(fd);
 }
 
+void EgaSpriteResource::appendFromFileAmiga(const char *filename) {
+	Common::File fd;
+	if (!fd.open(filename))
+		error("EgaSpriteResource::appendFromFileAmiga: cannot open %s", filename);
+	appendFromStreamAmiga(fd);
+}
+
+void EgaSpriteResource::appendFromStreamAmiga(Common::SeekableReadStream &stream) {
+	stream.skip(4); // skip 4-byte junk header
+
+	while (!stream.eos()) {
+		uint16 size = stream.readUint16BE();
+		if (stream.eos())
+			break;
+
+		byte h = stream.readByte(); // height in pixels
+		byte w = stream.readByte(); // width in 4-pixel units → actual pixel width = w * 4
+
+		if (size < 4)
+			break;
+
+		uint16 dataSize = size - 4; // bytes of word-planar pixel data
+
+		byte *data = new byte[dataSize];
+		uint16 read = stream.read(data, dataSize);
+
+		Graphics::Surface *sprite = new Graphics::Surface();
+		sprite->create(w * 4, h, Graphics::PixelFormat::createFormatCLUT8());
+
+		byte *pixels = (byte *)sprite->getPixels();
+
+		const uint16 rowBytes = w * 2; // w big-endian words per row
+		for (byte y = 0; y < h; y++) {
+			byte *dst = pixels + (uint)y * (w * 4);
+			for (byte c = 0; c < w; c++) {
+				uint off = (uint)y * rowBytes + c * 2;
+				uint16 word = (off + 1 < read) ? ((data[off] << 8) | data[off + 1]) : 0;
+				byte plane[4];
+				plane[0] = (word >> 12) & 0x0F;
+				plane[1] = (word >> 8) & 0x0F;
+				plane[2] = (word >> 4) & 0x0F;
+				plane[3] = word & 0x0F;
+				for (byte j = 0; j < 4; j++) {
+					byte bit = 3 - j; // MSB of each nibble is the leftmost pixel
+					byte ci = 0;
+					for (byte p = 0; p < 4; p++)
+						ci |= ((plane[p] >> bit) & 1) << p;
+					*dst++ = ci;
+				}
+			}
+		}
+
+		delete[] data;
+
+		_sprites.push_back(sprite);
+	}
+}
+
 void EgaSpriteResource::appendFromStream(Common::SeekableReadStream &stream) {
 	stream.skip(4); // skip 4-byte junk header
 
diff --git a/engines/chamber/ega_resource.h b/engines/chamber/ega_resource.h
index 3beee3b7938..85fbf968c8a 100644
--- a/engines/chamber/ega_resource.h
+++ b/engines/chamber/ega_resource.h
@@ -53,7 +53,11 @@ public:
 	void appendFromFile(const char *filename);
 	void appendFromStream(Common::SeekableReadStream &stream);
 
-	Graphics::Surface *getSprite(uint index) const { return _sprites[index]; }
+	void appendFromFileAmiga(const char *filename);
+	void appendFromStreamAmiga(Common::SeekableReadStream &stream);
+
+	// Returns a shared 1x1 placeholder when the bank has no such sprite
+	Graphics::Surface *getSprite(uint index) const;
 	uint getSpriteCount() const { return _sprites.size(); }
 
 private:
diff --git a/engines/chamber/kult.cpp b/engines/chamber/kult.cpp
index e3e03e2dd36..1c00a1c78ee 100644
--- a/engines/chamber/kult.cpp
+++ b/engines/chamber/kult.cpp
@@ -453,6 +453,25 @@ Common::Error ChamberEngine::init() {
 			exitGame();
 		/* fond wraps ega_backbuffer via init() — surface does not own pixel data, safe to delete directly */
 		delete fond;
+	} else if (g_vm->_videoMode == Common::kRenderAmiga) {
+		// Amiga sprite banks (appendFromFileAmiga); PUZZL/perso banks are merged here
+		ega_sprit_res = new EgaSpriteResource();
+		ega_sprit_res->appendFromFileAmiga("SPRIT.BIN");
+
+		ega_puzzl_res = new EgaSpriteResource();
+		ega_puzzl_res->appendFromFileAmiga("PUZZL.BIN");
+
+		ega_perso_res = new EgaSpriteResource();
+		ega_perso_res->appendFromFileAmiga("A.BIN");
+		ega_perso_res->appendFromFileAmiga("B.BIN");
+
+		Graphics::Surface *fond = loadFond();
+		if (!fond)
+			exitGame();
+		delete fond;
+
+		// Early intro screens set no palette of their own; apply the room palette now
+		amigaApplyRoomPalette(0);
 	} else {
 		while (!loadFond() || !loadSpritesData() || !loadPersData())
 			askDisk2();


Commit: 5e9c9d0ffd04da1937370879437963f449ac64d3
    https://github.com/scummvm/scummvm/commit/5e9c9d0ffd04da1937370879437963f449ac64d3
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Fix Amiga script padding and timer-thread gauss race

- Skip the 0x00/0xAA word-alignment padding the Amiga script blob inserts
  mid-instruction, which otherwise aborts scripts early (e.g. the Twins'
  serpent jaw redraw).
- Skip animateGauss on Amiga: it runs on the timer thread and blitting
  there races the main render thread.

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    engines/chamber/script.cpp
    engines/chamber/timer.cpp


diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index da855160a1d..a062c62f713 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -4495,6 +4495,13 @@ uint16 RunScript(byte *code) {
 #endif
 
 
+		// Amiga pads opcodes to word with 0x00 or 0xAA
+		if (g_vm->_videoMode == Common::kRenderAmiga
+		        && (opcode == 0x00 || opcode == 0xAA)) {
+			script_ptr++;
+			continue;
+		}
+
 		if (opcode == 0 || opcode >= MAX_SCR_HANDLERS)
 			break;
 
diff --git a/engines/chamber/timer.cpp b/engines/chamber/timer.cpp
index 392f307c7b6..1e85ffe3ccb 100644
--- a/engines/chamber/timer.cpp
+++ b/engines/chamber/timer.cpp
@@ -32,8 +32,8 @@ namespace Chamber {
 
 
 void animateGauss(byte *target) {
-	if (g_vm->_videoMode == Common::kRenderEGA)
-		return; /*gauss area is pre-rendered in FOND.EGA*/
+	if (isEgaLikeRenderer())
+		return; /*gauss is pre-rendered in FOND; on Amiga blitting here races the render thread*/
 	byte *sprite;
 	byte phase = getRand() % 4;
 	if (phase == script_byte_vars.gauss_phase)


Commit: b2cae9e8327d54f1dcf1c77f251bc862d995f76c
    https://github.com/scummvm/scummvm/commit/b2cae9e8327d54f1dcf1c77f251bc862d995f76c
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Minor Amiga review fixes

Use the platform (getPlatform() == kPlatformAmiga) instead of the derived
render mode for Amiga-specific branches, and reword the loadAmigaStaticData
comment in plain English.

Changed paths:
    engines/chamber/amiga.cpp
    engines/chamber/chamber.h
    engines/chamber/kult.cpp
    engines/chamber/metaengine.cpp
    engines/chamber/resdata.cpp
    engines/chamber/room.cpp
    engines/chamber/script.cpp


diff --git a/engines/chamber/amiga.cpp b/engines/chamber/amiga.cpp
index a9cd9dfca1d..83c9707c4f7 100644
--- a/engines/chamber/amiga.cpp
+++ b/engines/chamber/amiga.cpp
@@ -87,7 +87,9 @@ static AmigaResEnt amiga_res[] = {
 static const int kAmigaNumRes = sizeof(amiga_res) / sizeof(amiga_res[0]);
 
 int16 loadAmigaStaticData() {
-	// Idempotent: called before the title splash and again from loadStaticData()
+	// Safe to call more than once: it runs both before the title splash (to make
+	// the palette available) and again from loadStaticData(); the second call
+	// returns immediately.
 	if (amiga_palette_table != nullptr)
 		return 1;
 
diff --git a/engines/chamber/chamber.h b/engines/chamber/chamber.h
index 2e175cb8b76..1701b7cbc27 100644
--- a/engines/chamber/chamber.h
+++ b/engines/chamber/chamber.h
@@ -26,6 +26,7 @@
 #include "common/random.h"
 #include "common/serializer.h"
 #include "common/rendermode.h"
+#include "common/platform.h"
 #include "engines/engine.h"
 #include "chamber/renderer.h"
 
@@ -55,6 +56,7 @@ public:
 	~ChamberEngine();
 
 	Common::Language getLanguage() const;
+	Common::Platform getPlatform() const;
 
 	Common::Error run() override;
 	Common::Error init();
diff --git a/engines/chamber/kult.cpp b/engines/chamber/kult.cpp
index 1c00a1c78ee..1fc3f9d0258 100644
--- a/engines/chamber/kult.cpp
+++ b/engines/chamber/kult.cpp
@@ -301,7 +301,7 @@ Common::Error ChamberEngine::init() {
 
 	Graphics::Surface *splash = nullptr;
 
-	if (_videoMode == Common::kRenderAmiga) {
+	if (getPlatform() == Common::kPlatformAmiga) {
 		// Amiga title: static resources (incl. palette) live in KULT, load them first
 		loadAmigaStaticData();
 		splash = ega_loadFond("PRES.BIN");
@@ -453,7 +453,7 @@ Common::Error ChamberEngine::init() {
 			exitGame();
 		/* fond wraps ega_backbuffer via init() — surface does not own pixel data, safe to delete directly */
 		delete fond;
-	} else if (g_vm->_videoMode == Common::kRenderAmiga) {
+	} else if (g_vm->getPlatform() == Common::kPlatformAmiga) {
 		// Amiga sprite banks (appendFromFileAmiga); PUZZL/perso banks are merged here
 		ega_sprit_res = new EgaSpriteResource();
 		ega_sprit_res->appendFromFileAmiga("SPRIT.BIN");
diff --git a/engines/chamber/metaengine.cpp b/engines/chamber/metaengine.cpp
index 0148c1daff0..677c1aba537 100644
--- a/engines/chamber/metaengine.cpp
+++ b/engines/chamber/metaengine.cpp
@@ -34,6 +34,10 @@ Common::Language ChamberEngine::getLanguage() const {
 	return _gameDescription->language;
 }
 
+Common::Platform ChamberEngine::getPlatform() const {
+	return _gameDescription->platform;
+}
+
 } // end of namespace Chamber
 
 class ChamberMetaEngine : public AdvancedMetaEngine<ADGameDescription> {
diff --git a/engines/chamber/resdata.cpp b/engines/chamber/resdata.cpp
index 05e1e373d70..47ddef5e4d4 100644
--- a/engines/chamber/resdata.cpp
+++ b/engines/chamber/resdata.cpp
@@ -157,7 +157,7 @@ NB! Static data includes the font file, don't use any text print routines before
 */
 int16 loadStaticData() {
 	// Amiga keeps the static resources uncompressed inside the KULT executable
-	if (g_vm->_videoMode == Common::kRenderAmiga)
+	if (g_vm->getPlatform() == Common::kPlatformAmiga)
 		return loadAmigaStaticData();
 
 	Common::File pxi;
@@ -277,7 +277,7 @@ ResEntry_t res_texts_amiga[] = {
 Load strings data (commands/names)
 */
 int16 loadVepciData() {
-	if (g_vm->_videoMode == Common::kRenderAmiga)
+	if (g_vm->getPlatform() == Common::kPlatformAmiga)
 		return loadFilesList(res_texts_amiga);
 	return loadFilesList(res_texts);
 }
@@ -286,7 +286,7 @@ Graphics::Surface *loadFond(void) {
 	if (g_vm->_videoMode == Common::kRenderEGA)
 		return ega_loadFond("FOND.EGA");
 	// Amiga FOND.BIN is the same planar format as EGA
-	if (g_vm->_videoMode == Common::kRenderAmiga)
+	if (g_vm->getPlatform() == Common::kPlatformAmiga)
 		return ega_loadFond("FOND.BIN");
 	return loadSplash("FOND.BIN");
 }
@@ -328,7 +328,7 @@ ResEntry_t res_desci_amiga[] = {
 Load strings data (obj. descriptions)
 */
 int16 loadDesciData(void) {
-	if (g_vm->_videoMode == Common::kRenderAmiga)
+	if (g_vm->getPlatform() == Common::kPlatformAmiga)
 		return loadFilesList(res_desci_amiga);
 	while (!loadFilesList(res_desci))
 		askDisk2();
@@ -349,7 +349,7 @@ ResEntry_t res_diali_amiga[] = {
 Load strings data (dialogs)
 */
 int16 loadDialiData(void) {
-	if (g_vm->_videoMode == Common::kRenderAmiga)
+	if (g_vm->getPlatform() == Common::kPlatformAmiga)
 		return loadFilesList(res_diali_amiga);
 	while (!loadFilesList(res_diali))
 		askDisk2();
diff --git a/engines/chamber/room.cpp b/engines/chamber/room.cpp
index e44bbfdc8f1..381960ea277 100644
--- a/engines/chamber/room.cpp
+++ b/engines/chamber/room.cpp
@@ -160,7 +160,7 @@ static const byte cga_color_sels[] = {
 
 void selectSpecificPalette(byte index) {
 	// Amiga composes the room palette from the zone palette_index
-	if (g_vm->_videoMode == Common::kRenderAmiga)
+	if (g_vm->getPlatform() == Common::kPlatformAmiga)
 		amigaApplyRoomPalette(index);
 	else
 		g_vm->_renderer->colorSelect(cga_color_sels[index]);
@@ -168,7 +168,7 @@ void selectSpecificPalette(byte index) {
 
 
 void selectPalette(void) {
-	if (g_vm->_videoMode == Common::kRenderAmiga)
+	if (g_vm->getPlatform() == Common::kPlatformAmiga)
 		amigaApplyRoomPalette(script_byte_vars.palette_index);
 	else
 		g_vm->_renderer->colorSelect(cga_color_sels[script_byte_vars.palette_index]);
@@ -402,7 +402,7 @@ void loadZone(void) {
 	script_byte_vars.used_commands = 0;
 
 	// Apply the zone palette at load time for screens that skip refreshZone()
-	if (g_vm->_videoMode == Common::kRenderAmiga)
+	if (g_vm->getPlatform() == Common::kPlatformAmiga)
 		selectPalette();
 }
 
diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index a062c62f713..7f0d51aa324 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -4496,7 +4496,7 @@ uint16 RunScript(byte *code) {
 
 
 		// Amiga pads opcodes to word with 0x00 or 0xAA
-		if (g_vm->_videoMode == Common::kRenderAmiga
+		if (g_vm->getPlatform() == Common::kPlatformAmiga
 		        && (opcode == 0x00 || opcode == 0xAA)) {
 			script_ptr++;
 			continue;


Commit: 240eaeecab97b93fb26025d7c2510e7625c54190
    https://github.com/scummvm/scummvm/commit/240eaeecab97b93fb26025d7c2510e7625c54190
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Fix Amiga saucer animation and end screen

waitVBlank() only flushed the chunky SCREENBUFFER for EGA, so the in-place
saucer take-off animation never reached the Amiga screen; flush for any
EGA-like renderer. The end screen also decoded the planar Amiga PRES.BIN with
the CGA splash loader, turning it into a band of noise; load it with
ega_loadFond() and restore the title palette, as the title screen does.

Changed paths:
    engines/chamber/cga.cpp
    engines/chamber/script.cpp


diff --git a/engines/chamber/cga.cpp b/engines/chamber/cga.cpp
index 74e22f9a160..6df33ff38d2 100644
--- a/engines/chamber/cga.cpp
+++ b/engines/chamber/cga.cpp
@@ -142,7 +142,7 @@ void switchToTextMode(void) {
 
 void waitVBlank(void) {
 	pollInput();
-	if (g_vm->_videoMode == Common::kRenderEGA)
+	if (isEgaLikeRenderer())
 		g_vm->_renderer->blitToScreen(0, 0, EGA_WIDTH, EGA_HEIGHT);
 	g_system->delayMillis(10);
 	g_system->updateScreen();
diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index 7f0d51aa324..486841cfe67 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -30,6 +30,7 @@
 #include "chamber/cga.h"
 #include "chamber/ega.h"
 #include "chamber/ega_resource.h"
+#include "chamber/amiga.h"
 #include "chamber/cursor.h"
 #include "chamber/portrait.h"
 #include "chamber/input.h"
@@ -3022,6 +3023,11 @@ void theEnd(void) {
 			askDisk2();
 		jaggedZoom(backbuffer, frontbuffer);
 		g_vm->_renderer->backBufferToRealFull();
+	} else if (g_vm->getPlatform() == Common::kPlatformAmiga) {
+		while (!ega_loadFond("PRES.BIN"))
+			askDisk2();
+		g_vm->_renderer->colorSelect(AMIGA_NUM_PALETTES - 1);
+		g_vm->_renderer->backBufferToRealFull();
 	} else {
 		while (!loadSplash("PRES.BIN"))
 			askDisk2();


Commit: a5a88ba33d695ba83cfb102e51010113bde40a24
    https://github.com/scummvm/scummvm/commit/a5a88ba33d695ba83cfb102e51010113bde40a24
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Exit the end screen on input instead of hanging

The non-US end path looped forever with an empty busy-loop, which the host
reports as a frozen (not responding) application. Pump the event loop and
hold the end screen until the player clicks, presses a key, or closes the
window, then return to the launcher.

Changed paths:
    engines/chamber/script.cpp


diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index 486841cfe67..ddd1769b0f2 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -3041,10 +3041,17 @@ uint16 SCR_5B_TheEnd(void) {
 
 	theEnd();
 
-	if (g_vm->getLanguage() == Common::EN_USA)
+	if (g_vm->getLanguage() == Common::EN_USA) {
 		restartGame();
-	else
-		for (;;) ;  /*HANG*/
+	} else {
+		clearButtons();
+		do {
+			pollInputButtonsOnly();
+			g_system->delayMillis(10);
+			g_system->updateScreen();
+		} while (!g_vm->_shouldQuit && buttons == 0);
+		g_vm->_shouldQuit = true;
+	}
 
 	return 0;
 }


Commit: d40e898b3cf5c7f20aca0e8dc85661bed853e93e
    https://github.com/scummvm/scummvm/commit/d40e898b3cf5c7f20aca0e8dc85661bed853e93e
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Fix portrait animation pacing on modern hosts

The original busy-wait (delay * cpu_speed_delay iterations) collapses to ~0 on
fast CPUs, so portrait animations ran far too fast. Pace by wall-clock instead.

Changed paths:
    engines/chamber/portrait.cpp


diff --git a/engines/chamber/portrait.cpp b/engines/chamber/portrait.cpp
index 1dcc65b533c..8d9b2770e49 100644
--- a/engines/chamber/portrait.cpp
+++ b/engines/chamber/portrait.cpp
@@ -467,8 +467,7 @@ void animPortrait(byte layer, byte index, byte delay) {
 				else
 					blinkToWhite();
 			} else {
-				int16 i;
-				while (delay--) for (i = 0; i < cpu_speed_delay; i++) ; /*TODO: FIXME weak delay*/
+				g_system->delayMillis(delay * 1000 / 128);
 			}
 		}
 	}


Commit: c809bd0cb71c47fd1d39fa96201a4967b82fca7e
    https://github.com/scummvm/scummvm/commit/c809bd0cb71c47fd1d39fa96201a4967b82fca7e
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Fix the Amiga title palette

The title (PRES.BIN) used the table at KULT offset 122836, which is a
brightness fade-lookup ramp rather than RGB palette data, so the whole
title rendered in shades of red. Use the title's actual 16-colour palette
at offset 40896 instead.

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    engines/chamber/amiga.cpp
    engines/chamber/amiga.h


diff --git a/engines/chamber/amiga.cpp b/engines/chamber/amiga.cpp
index 83c9707c4f7..5b65d3aab16 100644
--- a/engines/chamber/amiga.cpp
+++ b/engines/chamber/amiga.cpp
@@ -41,7 +41,7 @@ struct AmigaResEnt {
 	uint32 size;
 };
 
-#define AMIGA_PAL_OFFSET 122836   // title fade ramp, end of LUTIN
+#define AMIGA_PAL_OFFSET 40896
 
 // Each room takes a shared base and drops its own 5-word delta over slots 1..5
 #define AMIGA_ROOM_PAL_OFFSET 40268
@@ -144,7 +144,7 @@ void amigaApplyPalette(byte index) {
 	if (index >= AMIGA_NUM_PALETTES)
 		return;
 
-	const byte *src = amiga_palette_table + index * (16 * 2);
+	const byte *src = amiga_palette_table;
 	byte palData[16 * 3];
 	for (int c = 0; c < 16; c++) {
 		uint16 w = (src[c * 2] << 8) | src[c * 2 + 1];   // big-endian 0x0RGB
diff --git a/engines/chamber/amiga.h b/engines/chamber/amiga.h
index 0687e807a51..c0fe6478764 100644
--- a/engines/chamber/amiga.h
+++ b/engines/chamber/amiga.h
@@ -35,7 +35,6 @@ public:
 
 #define AMIGA_NUM_PALETTES 31
 
-// 31x16 12-bit RGB title palette table inside the loaded KULT image
 extern const byte *amiga_palette_table;
 
 // Master's-Orbit full 16-colour palette (the one scene not using base+delta)


Commit: 350d2469f03f3bc3416531cf9af331ee52f92174
    https://github.com/scummvm/scummvm/commit/350d2469f03f3bc3416531cf9af331ee52f92174
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Fix borders for the rectangular UI boxes

Changed paths:
    engines/chamber/amiga.cpp
    engines/chamber/amiga.h


diff --git a/engines/chamber/amiga.cpp b/engines/chamber/amiga.cpp
index 5b65d3aab16..6d11a990fc4 100644
--- a/engines/chamber/amiga.cpp
+++ b/engines/chamber/amiga.cpp
@@ -26,6 +26,8 @@
 #include "chamber/chamber.h"
 #include "chamber/common.h"
 #include "chamber/amiga.h"
+#include "chamber/cga.h"
+#include "chamber/ega.h"
 #include "chamber/resdata.h"
 
 namespace Chamber {
@@ -59,7 +61,7 @@ static const uint16 amiga_room_base_pal[16] = {
 	0x000, // 4  stone
 	0x000, // 5  stone
 	0x101, // 6  crevice
-	0x100, // 7  dark detail
+	0xE14, // 7  bright red UI accent (room-name-bar / timer-box border)
 	0x886, // 8  stone/sprite highlight
 	0x64A, // 9  blue-purple accent
 	0x700, // 10 dark red
@@ -190,4 +192,26 @@ void AmigaRenderer::colorSelect(byte csel) {
 	amigaApplyPalette(csel);
 }
 
+static const byte amiga_cga_to_slot[4] = { 0, 11, 7, 15 };
+
+void AmigaRenderer::drawVLine(uint16 x, uint16 y, uint16 l, byte color, byte *target) {
+	byte pixel = amiga_cga_to_slot[color & 0x03];
+	uint16 ofs = calcXY(x, y);
+	for (uint16 i = 0; i < l; i++) {
+		target[ofs] = pixel;
+		ofs += EGA_BYTES_PER_LINE;
+	}
+
+	if (target == SCREENBUFFER)
+		blitToScreen(x, y, 1, l);
+}
+
+void AmigaRenderer::drawHLine(uint16 x, uint16 y, uint16 l, byte color, byte *target) {
+	byte pixel = amiga_cga_to_slot[color & 0x03];
+	memset(target + calcXY(x, y), pixel, l);
+
+	if (target == SCREENBUFFER)
+		blitToScreen(x, y, l, 1);
+}
+
 } // End of namespace Chamber
diff --git a/engines/chamber/amiga.h b/engines/chamber/amiga.h
index c0fe6478764..bf9ed0be932 100644
--- a/engines/chamber/amiga.h
+++ b/engines/chamber/amiga.h
@@ -31,6 +31,8 @@ public:
 	void switchToGraphicsMode() override;
 	void colorSelect(byte csel) override;     // csel is the real palette index
 	void selectCursor(uint16 num) override;   // SOURI.BIN: big-endian cursor planes
+	void drawVLine(uint16 x, uint16 y, uint16 l, byte color, byte *target) override;
+	void drawHLine(uint16 x, uint16 y, uint16 l, byte color, byte *target) override;
 };
 
 #define AMIGA_NUM_PALETTES 31


Commit: 1dd797d863ab204a2a9f8330478d097b393b2f5a
    https://github.com/scummvm/scummvm/commit/1dd797d863ab204a2a9f8330478d097b393b2f5a
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-30T01:10:47+02:00

Commit Message:
CHAMBER: Fix warning message at character presentation

The EGA/Amiga dot-dissolve portrait effect ran a long pixel loop without
pumping the host event loop, so a run of these effects starved the OS event
queue and the app was reported as not responding. Poll input periodically
during the dissolve.

Changed paths:
    engines/chamber/script.cpp


diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index ddd1769b0f2..3af5a91302e 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -877,8 +877,13 @@ uint16 SCR_D_DrawPortraitDotEffect(void) {
 			uint16 py = offs / pw;
 			target[baseOfs + py * EGA_BYTES_PER_LINE + px] = cur_image_pixels[offs];
 
-			if (count % 20 == 0)
+			if (count % 20 == 0) {
 				g_vm->_renderer->blitToScreen(x * 4, y, pw, height);
+				pollInput();
+				g_system->updateScreen();
+				if (g_vm->_shouldQuit)
+					return 0;
+			}
 
 			offs += step;
 			if (offs > cur_image_end)
@@ -902,8 +907,15 @@ uint16 SCR_D_DrawPortraitDotEffect(void) {
 	for (offs = 0; offs != cur_image_end;) {
 		target[g_vm->_renderer->calcXY_p(x + offs % cur_image_size_w, y + offs / cur_image_size_w)] = cur_image_pixels[offs]; // TODO check this
 
-		if (count % 5 == 0)
+		if (count % 5 == 0) {
 			g_vm->_renderer->blitToScreen(offs, g_vm->_screenPPB, 1);
+			if ((count % 100) == 0) {
+				pollInput();
+				g_system->updateScreen();
+				if (g_vm->_shouldQuit)
+					return 0;
+			}
+		}
 
 		offs += step;
 		if (offs > cur_image_end)




More information about the Scummvm-git-logs mailing list