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

bluegr noreply at scummvm.org
Sun Sep 27 09:47:26 UTC 2026


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

Summary:
b99f364869 IMAGE: Refactor CDToons decoder


Commit: b99f3648690110b4655e907589741701a35ead12
    https://github.com/scummvm/scummvm/commit/b99f3648690110b4655e907589741701a35ead12
Author: Hajin Jang (jb6804 at naver.com)
Date: 2026-09-27T12:47:21+03:00

Commit Message:
IMAGE: Refactor CDToons decoder

Refactor CDToons decoder based on the original decoder binary.

- The reference binary QB32.QTC used in research was taken from the Logical Journey of the Zoombinis v1.11KR.
- The revised code was battle-tested against 528 CDToons MOV samples from every possible Mohawk game.

Assisted-by: Codex:gpt-5.6

Changed paths:
  A test/image/cdtoons.h
    image/codecs/cdtoons.cpp
    image/codecs/cdtoons.h


diff --git a/image/codecs/cdtoons.cpp b/image/codecs/cdtoons.cpp
index b0a027ccc1f..c425bf95f7e 100644
--- a/image/codecs/cdtoons.cpp
+++ b/image/codecs/cdtoons.cpp
@@ -27,17 +27,60 @@
 
 namespace Image {
 
-struct CDToonsAction {
-	uint16 blockId;
-	Common::Rect rect;
-};
-
 struct CDToonsDiff {
 	byte *data;
 	uint32 size;
 	Common::Rect rect;
 };
 
+enum CDToonsBlockFlags : uint32 {
+	/**
+	 * Identifies palette data.
+	 */
+	kCDToonsBlockTypePalette = 0x01,
+	/**
+	 * Identifies RLE image data.
+	 */
+	kCDToonsBlockTypeImage = 0x02,
+	kCDToonsBlockTypeMask = kCDToonsBlockTypePalette | kCDToonsBlockTypeImage,
+	/**
+	 * Marks a block header that has already been converted to host byte order.
+	 */
+	kCDToonsBlockFlagEndianConverted = 0x80,
+};
+
+enum CDToonsFrameFlags : uint32 {
+	/**
+	 * Declares the presence of the corresponding optional frame subchunk.
+	 */
+	kCDToonsFrameFlagHasMarkers = 0x02,
+	/**
+	 * Skips dirty-region clearing so transparent RLE runs retain existing pixels.
+	 */
+	kCDToonsFrameFlagPreservePreviousPixels = 0x08,
+	kCDToonsFrameFlagHasXFrm = 0x10,
+	kCDToonsFrameFlagHasBackgroundRects = 0x20,
+	kCDToonsFrameFlagHasForegroundRects = 0x40,
+	/**
+	 * Marks a frame header that has already been converted to host byte order.
+	 */
+	kCDToonsFrameFlagEndianConverted = 0x80,
+};
+
+static const uint16 kCDToonsFrameFormat = 9;
+static const byte kCDToonsActionSize = 10;
+
+static const char *getBlockTypeName(uint16 typeFlags) {
+	switch (typeFlags & kCDToonsBlockTypeMask) {
+	case kCDToonsBlockTypePalette:
+		return "palette";
+	case kCDToonsBlockTypeImage:
+		return "image";
+	default:
+		return "unknown";
+	}
+}
+
 static Common::Rect readRect(Common::SeekableReadStream &stream) {
 	Common::Rect rect;
 	rect.top = stream.readUint16BE();
@@ -52,6 +95,9 @@ CDToonsDecoder::CDToonsDecoder(uint16 width, uint16 height) : _palette(256) {
 
 	_surface = new Graphics::Surface();
 	_surface->create(width, height, Graphics::PixelFormat::createFormatCLUT8());
+	_backingSurface = new Graphics::Surface();
+	_backingSurface->create(width, height, Graphics::PixelFormat::createFormatCLUT8());
+	_backingSurfaceValid = false;
 
 	_currentPaletteId = 0;
 	_dirtyPalette = false;
@@ -60,61 +106,92 @@ CDToonsDecoder::CDToonsDecoder(uint16 width, uint16 height) : _palette(256) {
 CDToonsDecoder::~CDToonsDecoder() {
 	_surface->free();
 	delete _surface;
+	_backingSurface->free();
+	delete _backingSurface;
 
 	for (auto &block : _blocks)
 		delete[] block._value.data;
 }
 
 Graphics::Surface *CDToonsDecoder::decodeFrame(Common::SeekableReadStream &stream) {
-	uint16 u0 = stream.readUint16BE(); // always 9?
+	// (0x00) The first word identifies the frame-header format.
+	// There is only one CDToons frame format: 9.
+	uint16 frameFormat = stream.readUint16BE();
+	if (frameFormat != kCDToonsFrameFormat)
+		error("CDToons: unsupported frame format %d", frameFormat);
+
+	// (0x02) Sequential frame ID.
 	uint16 frameId = stream.readUint16BE();
+	// (0x04) Last frame through which newly supplied block records remain valid.
 	uint16 blocksValidUntil = stream.readUint16BE();
-	byte u6 = stream.readByte();
+	// This authoring hint identifies a primary action involved in the frame.
+	// (0x06) It does not control rendering; only actions and block IDs do.
+	byte primaryActionIndexHint = stream.readByte();
+	// (0x07) Palette index used when clearing frame regions.
 	byte backgroundColor = stream.readByte();
-	debugN(5, "CDToons frame %d, size %d, unknown %04x (at 0), blocks valid until %d, unknown 6 is %02x, bkg color is %02x\n",
-		frameId, (int)stream.size(), u0, blocksValidUntil, u6, backgroundColor);
+	debugN(5, "CDToons frame %d, size %d, format %d, blocks valid until %d, primary action %d, bkg color is %02x\n",
+		frameId, (int)stream.size(), frameFormat, blocksValidUntil, primaryActionIndexHint, backgroundColor);
 
+	// (0x08) Frame clip rectangle.
 	Common::Rect clipRect = readRect(stream);
 	debugN(9, "CDToons clipRect: (%d, %d) to (%d, %d)\n",
 		clipRect.left, clipRect.top, clipRect.right, clipRect.bottom);
 
+	// (0x10) Frame output dirty rectangle.
 	Common::Rect dirtyRect = readRect(stream);
 	debugN(9, "CDToons dirtyRect: (%d, %d) to (%d, %d)\n",
 		dirtyRect.left, dirtyRect.top, dirtyRect.right, dirtyRect.bottom);
 
+	// (0x18) Optional-subchunk and compositing flags.
 	uint32 flags = stream.readUint32BE();
-	if (flags & 0x80)
-		error("CDToons: frame already processed?");
+	if (flags & kCDToonsFrameFlagEndianConverted)
+		error("CDToons: frame was already endian-converted");
 	debugN(5, "CDToons flags: %08x\n", flags);
 
+	// (0x1C) Number of block records supplied by this frame.
 	uint16 blockCount = stream.readUint16BE();
+	// (0x1E) Offset from the frame start to the first block record.
 	uint16 blockOffset = stream.readUint16BE();
 	debugN(9, "CDToons: %d blocks at 0x%04x\n",
 		blockCount, blockOffset);
 
-	// max block id?
-	uint16 u32 = stream.readUint16BE();
-	debugN(5, "CDToons unknown at 32: %04x\n", u32);
+	// (0x20) The max block ID used by block records and references.
+	uint16 maxBlockId = stream.readUint16BE();
+	debugN(5, "CDToons highest block ID: %d\n", maxBlockId);
 
+	// (0x22) Number of action records in the frame header.
 	byte actionCount = stream.readByte();
-	byte u35 = stream.readByte();
+	// (0x23) The stream declares the size of each action record.
+	byte actionSize = stream.readByte();
+	if (actionSize != kCDToonsActionSize) // CDToons uses a fixed 10-byte layout.
+		error("CDToons: unsupported action size %d", actionSize);
 
+	// (0x24) Block ID of the palette selected for this frame.
 	uint16 paletteId = stream.readUint16BE();
-	byte paletteSet = stream.readByte();
-	debugN(9, "CDToons palette id %04x, palette byte %02x\n",
-		paletteId, paletteSet);
-
-	byte u39 = stream.readByte();
-	uint16 u40 = stream.readUint16BE();
-	uint16 u42 = stream.readUint16BE();
-	debugN(5, "CDToons: unknown at 35 is %02x, unknowns at 39: %02x, %04x, %04x\n",
-		u35, u39, u40, u42);
+	if (paletteId && maxBlockId < paletteId)
+		error("CDToons: palette block ID %d exceeded declared maximum %d", paletteId, maxBlockId);
+	// (0x26) A nonzero value suppresses palette realization for 8-bit indexed-color output.
+	uint16 paletteAlreadySet = stream.readUint16BE();
+	// (0x28) This reserved word is copied with the cached action descriptor but never consumed.
+	uint16 reserved40 = stream.readUint16BE();
+	// (0x2A) This revision decides whether a repeated frame ID with paletteAlreadySet should be composited again.
+	uint16 sameFrameRevision = stream.readUint16BE();
+	debugN(5, "CDToons: action size %d, palette id %04x, palette already set %04x, reserved %04x, same-frame revision %04x\n",
+		actionSize, paletteId, paletteAlreadySet, reserved40, sameFrameRevision);
 
 	Common::Array<CDToonsAction> actions;
 
+	// Actions reference cached image blocks by ID.
+	// - The rectangle supplies the placement and validity fields.
+	// - The encoded block supplies its dimensions.
+	// The first entry starts at frame offset 0x2C; offsets below are entry-relative.
 	for (uint i = 0; i < actionCount; i++) {
 		CDToonsAction action;
+		// (0x00) Cached image block ID.
 		action.blockId = stream.readUint16BE();
+		if (action.blockId && action.blockId != 0xffff && maxBlockId < action.blockId)
+			error("CDToons: action block ID %d exceeded declared maximum %d", action.blockId, maxBlockId);
+		// (0x02) Action placement and validity rectangle.
 		action.rect = readRect(stream);
 		debugN(9, "CDToons action: render block %d at (%d, %d) to (%d, %d)\n",
 			action.blockId, action.rect.left, action.rect.top, action.rect.right, action.rect.bottom);
@@ -128,75 +205,121 @@ Graphics::Surface *CDToonsDecoder::decodeFrame(Common::SeekableReadStream &strea
 	if (stream.pos() != blockOffset)
 		error("CDToons had %d unknown bytes after header", blockOffset - (int)stream.pos());
 
+	// A block record contains an ID, flags, declared size, frame lifetime, and payload.
+	// - Payloads referenced by paletteId are palettes.
+	// - Action payloads contain an image header followed by scanline RLE data.
+	// Offsets below are relative to the block record selected by blockOffset.
 	for (uint i = 0; i < blockCount; i++) {
+		// (0x00) Block ID used by palette and action references.
 		uint16 blockId = stream.readUint16BE();
-		if (blockId >= 1200)
-			error("CDToons: block id %d was too high", blockId);
+		if (blockId > maxBlockId)
+			error("CDToons: block ID %d exceeded declared maximum %d", blockId, maxBlockId);
 		if (_blocks.contains(blockId))
 			error("CDToons: new block %d was already seen", blockId);
 
 		CDToonsBlock block;
-		block.flags = stream.readUint16BE();
-		// flag 1 = palette, flag 2 = data?
-		if (block.flags & 0x8000)
-			error("CDToons: block already processed?");
+		// (0x02) Payload type and storage flags.
+		block.typeFlags = stream.readUint16BE();
+		if (block.typeFlags & kCDToonsBlockFlagEndianConverted)
+			error("CDToons: block was already endian-converted");
+		// (0x04) Total block-record size, including this fixed header.
 		block.size = stream.readUint32BE();
 		if (block.size < 14)
 			error("CDToons: block size was %d, too small", block.size);
 		block.size -= 14;
+		// (0x08) First frame for which the cached block is valid.
 		block.startFrame = stream.readUint16BE();
+		// (0x0A) Last frame for which the cached block is valid.
 		block.endFrame = stream.readUint16BE();
-		block.unknown12 = stream.readUint16BE();
+		// (0x0C) Hints the block's primary action slot. Does not affect image decoding.
+		byte blockPrimaryActionIndexHint = stream.readByte();
+		// (0x0D) Reserved byte that does not affect image decoding.
+		byte blockReserved13 = stream.readByte();
 		block.data = new byte[block.size];
+		// (0x0E) Palette or image payload.
 		stream.read(block.data, block.size);
 
-		debugN(9, "CDToons block id 0x%04x of size 0x%08x, flags %04x, from frame %d to %d, unknown at 12 is %04x\n",
-			blockId, block.size, block.flags, block.startFrame, block.endFrame, block.unknown12);
+		debugN(9, "CDToons block id 0x%04x of size 0x%08x, type %s, flags %04x, from frame %d to %d, primary action %d, reserved %02x\n",
+			blockId, block.size, getBlockTypeName(block.typeFlags), block.typeFlags,
+			block.startFrame, block.endFrame, blockPrimaryActionIndexHint, blockReserved13);
 
 		_blocks[blockId] = block;
 	}
 
-	byte xFrmBegin = 0, xFrmCount;
+	bool hasXFrm = false;
+	byte xFrmBegin = 0;
+	byte xFrmCount = 0;
+	Common::Rect xFrmRect;
 	Common::Array<CDToonsDiff> diffs;
+	Common::Array<Common::Rect> backgroundRects;
+	Common::Array<Common::Rect> foregroundRects;
 
 	while (true) {
 		int32 nextPos = stream.pos();
+		// Offsets are relative to the tagged block.
+		// (0x00) Four-character block type.
 		uint32 tag = stream.readUint32BE();
+		// (0x04) Total tagged-block size, including the tag and this field.
 		uint32 size = stream.readUint32BE();
+		if (size < 8)
+			error("CDToons: block '%s' size was %d, too small", tag2str(tag), size);
 		nextPos += size;
+		if (stream.size() < nextPos)
+			error("CDToons: block '%s' ran past the frame", tag2str(tag));
 
 		switch (tag) {
 		case MKTAG('D','i','f','f'):
 			{
+			// [Diff] contains a count and aggregate clip rectangle, followed by variable-sized replacement rectangles.
+			// Each entry stores its bounds, total entry size, dimensions, two reserved words, and scanline RLE.
+			// Diff entries patch the output directly instead of using frame actions.
 			debugN(5, "CDToons: Diff\n");
+			if (nextPos - stream.pos() < 10)
+				error("CDToons: Diff block was too small");
+			// (0x08) Number of replacement-rectangle entries.
 			uint16 count = stream.readUint16BE();
 
+			// (0x0A) Aggregate clip rectangle for the replacement entries.
 			Common::Rect diffClipRect = readRect(stream);
 			debugN(9, "CDToons diffClipRect: (%d, %d) to (%d, %d)\n",
 				diffClipRect.left, diffClipRect.top, diffClipRect.right, diffClipRect.bottom);
 
 			debugN(5, "CDToons Diff: %d subentries\n", count);
+			// The first entry starts at block offset 0x12; offsets below are entry-relative.
 			for (uint i = 0; i < count; i++) {
 				CDToonsDiff diff;
+				int32 entryStart = stream.pos();
+				if (nextPos - entryStart < 20)
+					error("CDToons: Diff entry %d header ran past the block", i);
 
+				// (0x00) Replacement rectangle.
 				diff.rect = readRect(stream);
-				diff.size = stream.readUint32BE();
-				if (diff.size < 20)
-					error("CDToons: Diff block size was %d, too small", diff.size);
-
+				// (0x08) Total size of this variable-sized entry.
+				uint32 entrySize = stream.readUint32BE();
+				if (entrySize < 20)
+					error("CDToons: Diff entry size was %d, too small", entrySize);
+				if (static_cast<uint32>(nextPos - entryStart) < entrySize)
+					error("CDToons: Diff entry %d ran past the block", i);
+
+				// (0x0C) Width that duplicates the rectangle.
 				uint16 diffWidth = stream.readUint16BE();
+				// (0x0E) Height that duplicates the rectangle.
 				uint16 diffHeight = stream.readUint16BE();
-				uint16 unknown16 = stream.readUint16BE();
-				uint16 unknown18 = stream.readUint16BE();
-				diff.size -= 8;
+				// The render size is derived from the rectangle, and all four words are otherwise skipped.
+				// (0x10) First reserved word.
+				uint16 reserved16 = stream.readUint16BE();
+				// (0x12) Second reserved word.
+				uint16 reserved18 = stream.readUint16BE();
+				diff.size = entrySize - 20;
 
 				if (diffWidth != diff.rect.width() || diffHeight != diff.rect.height())
 					error("CDToons: Diff sizes didn't match");
-				debugN(5, "CDToons Diff: size %d, frame from (%d, %d) to (%d, %d), unknowns %04x, %04x\n",
+				debugN(5, "CDToons Diff: size %d, frame from (%d, %d) to (%d, %d), reserved %04x, %04x\n",
 					diff.size, diff.rect.left, diff.rect.top, diff.rect.right, diff.rect.bottom,
-					unknown16, unknown18);
+					reserved16, reserved18);
 
 				diff.data = new byte[diff.size];
+				// (0x14) Scanline RLE payload.
 				stream.read(diff.data, diff.size);
 				diffs.push_back(diff);
 			}
@@ -204,52 +327,120 @@ Graphics::Surface *CDToonsDecoder::decodeFrame(Common::SeekableReadStream &strea
 			break;
 		case MKTAG('X','F','r','m'):
 			{
+			// [XFrm] stores a one-based action start, an action count, one retained background update/clip rectangle, and optional reserved trailing data.
+			// The selected actions update the clean background;
+			// later actions are foreground overlays after that background is restored to the output.
 			debugN(5, "CDToons: XFrm\n");
-			if (!(flags & 0x10))
-				error("CDToons: useless XFrm?");
+			if (!(flags & kCDToonsFrameFlagHasXFrm))
+				error("CDToons: XFrm block was not declared by frame flags");
 
-			if (xFrmBegin)
+			if (hasXFrm)
 				error("CDToons: duplicate XFrm");
+			if (nextPos - stream.pos() < 10)
+				error("CDToons: XFrm block was too small");
+
+			hasXFrm = true;
+			// (0x08) One-based index of the first retained-background action.
 			xFrmBegin = stream.readByte();
+			// (0x09) Number of retained-background actions.
 			xFrmCount = stream.readByte();
 			debugN(9, "CDToons XFrm: run %d actions from %d\n", xFrmCount, xFrmBegin - 1);
 
-			// TODO: don't ignore (if xFrmCount is non-zero)
-			Common::Rect dirtyRectXFrm = readRect(stream);
+			// (0x0A) The selected actions update a retained background layer inside this rectangle.
+			xFrmRect = readRect(stream);
 			debugN(9, "CDToons XFrm dirtyRect: (%d, %d) to (%d, %d)\n",
-				dirtyRectXFrm.left, dirtyRectXFrm.top, dirtyRectXFrm.right, dirtyRectXFrm.bottom);
+				xFrmRect.left, xFrmRect.top, xFrmRect.right, xFrmRect.bottom);
 
-			// always zero?
-			Common::Rect dirtyRect2XFrm = readRect(stream);
-			debugN(9, "CDToons XFrm dirtyRect2: (%d, %d) to (%d, %d)\n",
-				dirtyRect2XFrm.left, dirtyRect2XFrm.top, dirtyRect2XFrm.right, dirtyRect2XFrm.bottom);
+			// Bytes after the update rectangle are reserved extension data.
+			// They are not a second rectangle.
+			if (stream.pos() != nextPos) {
+				debugN(9, "CDToons XFrm: skipping %d reserved bytes\n", nextPos - (int32)stream.pos());
+				stream.seek(nextPos);
+			}
 			}
 			break;
 		case MKTAG('M','r','k','s'):
+			{
+			// [Mrks] contains cues attached to this point in the video timeline.
+			// [PreF] and [PstF] cues notify the player immediately before or after the associated frame is rendered,
+			// allowing an external application to synchronize its own actions with video playback.
+			// This block does not have rendering commands and do not change the decoded image.
+
+			// LOGO025.MOV from Logical Journey of the Zoombinis v1.11KR contains this block, but the game never uses the callback feature.
+			// The actual data does not have any valid [PreF] or [PstF] tags, too.
+			// Thus, ScummVM decoder does not implement a callback system for such timeline cues.
 			debugN(5, "CDToons: Mrks\n");
-			if (!(flags & 0x2))
-				error("CDToons: useless Mrks?");
-
-			// TODO
-			warning("CDToons: encountered Mrks, not implemented yet");
+			if (!(flags & kCDToonsFrameFlagHasMarkers))
+				error("CDToons: Mrks block was not declared by frame flags");
+
+			if (nextPos - stream.pos() < 2)
+				error("CDToons: Mrks block was too small");
+
+			// (0x08) Number of timeline-marker entries.
+			uint16 markerCount = stream.readUint16BE();
+			debugN(9, "CDToons Mrks: %d markers\n", markerCount);
+			// The first entry starts at block offset 0x0A; offsets below are entry-relative.
+			for (uint i = 0; i < markerCount; i++) {
+				if (nextPos - stream.pos() < 8)
+					error("CDToons: Mrks entry %d header ran past the block", i);
+
+				// (0x00) Four-character marker type.
+				uint32 markerTag = stream.readUint32BE();
+				// (0x04) Total marker-entry size, including this header.
+				uint32 markerSize = stream.readUint32BE();
+				if (markerSize < 8)
+					error("CDToons: Mrks entry %d size was %d, too small", i, markerSize);
+				if (nextPos - stream.pos() < markerSize - 8)
+					error("CDToons: Mrks entry %d ran past the block", i);
+
+				// [PreF] and [PstF] select pre-render and post-render delivery.
+				if (markerTag == MKTAG('P','r','e','F') || markerTag == MKTAG('P','s','t','F')) {
+					if (markerSize < 12)
+						error("CDToons: callback marker '%s' was too small", tag2str(markerTag));
+
+					// (0x08) Size of the application callback payload.
+					uint32 callbackDataSize = stream.readUint32BE();
+					if (markerSize - 12 < callbackDataSize)
+						error("CDToons: callback marker '%s' data ran past the entry", tag2str(markerTag));
+					debugN(9, "CDToons marker '%s': entry size %d, callback data size %d\n",
+						tag2str(markerTag), markerSize, callbackDataSize);
+					stream.skip(markerSize - 12);
+				} else {
+					debugN(9, "CDToons marker tag %08x: entry size %d, ignored\n", markerTag, markerSize);
+					stream.skip(markerSize - 8);
+				}
+			}
+			}
 			break;
 		case MKTAG('S','c','a','l'):
-			// TODO
-			warning("CDToons: encountered Scal, not implemented yet");
+			// [Scal] is an opaque extension payload.
+			debugN(5, "CDToons: skipping opaque Scal block (%d payload bytes)\n", size - 8);
+			stream.seek(nextPos);
 			break;
 		case MKTAG('W','r','M','p'):
-			warning("CDToons: encountered WrMp, ignoring");
+			// [WrMp] is an opaque extension payload.
+			debugN(5, "CDToons: skipping opaque WrMp block (%d payload bytes)\n", size - 8);
+			stream.seek(nextPos);
 			break;
 		case MKTAG('F','r','t','R'):
 			{
+			// [FrtR] contains a count followed by output dirty rectangles.
+			// They limit foreground/output clearing and the final transfer to the host surface.
 			debugN(5, "CDToons: FrtR\n");
-			if (!(flags & 0x40))
-				error("CDToons: useless FrtR?");
+			if (!(flags & kCDToonsFrameFlagHasForegroundRects))
+				error("CDToons: FrtR block was not declared by frame flags");
+			if (nextPos - stream.pos() < 2)
+				error("CDToons: FrtR block was too small");
 
+			// (0x08) Number of output dirty rectangles.
 			uint16 count = stream.readUint16BE();
+			if ((nextPos - stream.pos()) / 8 < count)
+				error("CDToons: FrtR rectangle list ran past the block");
 			debugN(9, "CDToons FrtR: %d dirty rectangles\n", count);
 			for (uint i = 0; i < count; i++) {
+				// (0x0A + i * 0x08) Output dirty rectangle.
 				Common::Rect dirtyRectFrtR = readRect(stream);
+				foregroundRects.push_back(dirtyRectFrtR);
 				debugN(9, "CDToons FrtR dirtyRect: (%d, %d) to (%d, %d)\n",
 					dirtyRectFrtR.left, dirtyRectFrtR.top, dirtyRectFrtR.right, dirtyRectFrtR.bottom);
 			}
@@ -257,14 +448,23 @@ Graphics::Surface *CDToonsDecoder::decodeFrame(Common::SeekableReadStream &strea
 			break;
 		case MKTAG('B','c','k','R'):
 			{
+			// [BckR] contains a count followed by retained-background dirty rectangles.
+			// When clearing is enabled, these regions are reset to the frame background color before the XFrm action range updates them.
 			debugN(5, "CDToons: BckR\n");
-			if (!(flags & 0x20))
-				error("CDToons: useless BckR?");
+			if (!(flags & kCDToonsFrameFlagHasBackgroundRects))
+				error("CDToons: BckR block was not declared by frame flags");
+			if (nextPos - stream.pos() < 2)
+				error("CDToons: BckR block was too small");
 
+			// (0x08) Number of retained-background dirty rectangles.
 			uint16 count = stream.readUint16BE();
+			if ((nextPos - stream.pos()) / 8 < count)
+				error("CDToons: BckR rectangle list ran past the block");
 			debugN(9, "CDToons BckR: %d subentries\n", count);
 			for (uint i = 0; i < count; i++) {
+				// (0x0A + i * 0x08) Retained-background dirty rectangle.
 				Common::Rect dirtyRectBckR = readRect(stream);
+				backgroundRects.push_back(dirtyRectBckR);
 				debugN(9, "CDToons BckR dirtyRect: (%d, %d) to (%d, %d)\n",
 					dirtyRectBckR.left, dirtyRectBckR.top, dirtyRectBckR.right, dirtyRectBckR.bottom);
 			}
@@ -287,30 +487,63 @@ Graphics::Surface *CDToonsDecoder::decodeFrame(Common::SeekableReadStream &strea
 	}
 
 	for (uint i = 0; i < diffs.size(); i++) {
-		renderBlock(diffs[i].data, diffs[i].size, diffs[i].rect.left, diffs[i].rect.top, diffs[i].rect.width(), diffs[i].rect.height());
-		delete[] diffs[i].data;
+		Image::CDToonsDiff &diff = diffs[i];
+		renderBlock(*_surface, diff.data, diff.size, diff.rect.left, diff.rect.top, diff.rect.width(), diff.rect.height());
+		delete[] diff.data;
 	}
 	if (!diffs.empty())
 		return _surface;
 
-	for (uint i = 0; i < actions.size(); i++) {
-		CDToonsAction &action = actions[i];
-		if (i == 0 && action.blockId == 0)
-			memset(_surface->getPixels(), backgroundColor, _surface->w * _surface->h);
-		if (!_blocks.contains(action.blockId))
-			continue;
-		if (!action.rect.right)
-			continue;
-		if (i == 0 && !diffs.empty())
-			continue;
+	int xFrmStart = static_cast<int8>(xFrmBegin) - 1;
+	if (xFrmStart < 0)
+		xFrmStart = 0;
+	uint backgroundBegin = MIN<uint>(xFrmStart, actions.size());
+	uint backgroundEnd = MIN<uint>(backgroundBegin + xFrmCount, actions.size());
 
-		CDToonsBlock &block = _blocks[action.blockId];
-		uint16 width = READ_BE_UINT16(block.data + 2);
-		uint16 height = READ_BE_UINT16(block.data);
+	if (hasXFrm && xFrmCount && !xFrmRect.isEmpty()) {
+		if (!_backingSurfaceValid) {
+			_backingSurface->fillRect(_backingSurface->getRect(), backgroundColor);
+			_backingSurfaceValid = true;
+		}
+
+		// Repeated RLE runs of color zero are transparent and leave destination pixels unchanged.
+		// Clear dirty background regions unless this frame asks to retain the previous pixels through those transparent runs.
+		if (!(flags & kCDToonsFrameFlagPreservePreviousPixels)) {
+			if (!backgroundRects.empty()) {
+				for (const Common::Rect &rect : backgroundRects)
+					_backingSurface->fillRect(rect, backgroundColor);
+			} else {
+				_backingSurface->fillRect(xFrmRect, backgroundColor);
+			}
+		}
+		renderActions(actions, backgroundBegin, backgroundEnd, *_backingSurface, &xFrmRect);
+	} else if (frameId == 1 && !actions.empty() && actions[0].blockId && actions[0].blockId != 0xffff) {
+		if (!_backingSurfaceValid) {
+			_backingSurface->fillRect(_backingSurface->getRect(), backgroundColor);
+			_backingSurfaceValid = true;
+		}
+		renderActions(actions, 0, 1, *_backingSurface, nullptr);
+	}
 
-		renderBlock(block.data + 14, block.size - 14, action.rect.left, action.rect.top, width, height);
+	if (_backingSurfaceValid) {
+		const Common::Rect clippedDirtyRect = dirtyRect.findIntersectingRect(_surface->getRect());
+		if (!clippedDirtyRect.isEmpty()) {
+			Graphics::Surface outputArea = _surface->getSubArea(clippedDirtyRect);
+			const Graphics::Surface backingArea = _backingSurface->getSubArea(clippedDirtyRect);
+			outputArea.copyRectToSurface(backingArea, 0, 0, backingArea.getRect());
+		}
+	} else if (!(flags & kCDToonsFrameFlagPreservePreviousPixels)) {
+		if (!foregroundRects.empty()) {
+			for (const Common::Rect &rect : foregroundRects)
+				_surface->fillRect(rect, backgroundColor);
+		} else {
+			_surface->fillRect(dirtyRect, backgroundColor);
+		}
 	}
 
+	uint foregroundBegin = hasXFrm ? backgroundEnd : MIN<uint>(1, actions.size());
+	renderActions(actions, foregroundBegin, actions.size(), *_surface, nullptr);
+
 	if (paletteId && _currentPaletteId != paletteId) {
 		if (!_blocks.contains(paletteId))
 			error("CDToons: no block for palette %04x", paletteId);
@@ -318,105 +551,122 @@ Graphics::Surface *CDToonsDecoder::decodeFrame(Common::SeekableReadStream &strea
 			error("CDToons: palette %04x is wrong size (%d)", paletteId, _blocks[paletteId].size);
 
 		_currentPaletteId = paletteId;
-		if (!paletteSet)
+		if (!paletteAlreadySet)
 			setPalette(_blocks[paletteId].data);
 	}
 
 	return _surface;
 }
 
-void CDToonsDecoder::renderBlock(byte *data, uint dataSize, int destX, int destY, uint width, uint height) {
+void CDToonsDecoder::renderActions(const Common::Array<CDToonsAction> &actions, uint begin, uint end, Graphics::Surface &surface, const Common::Rect *clipRect) {
+	end = MIN<uint>(end, actions.size());
+	for (uint i = begin; i < end; i++) {
+		const CDToonsAction &action = actions[i];
+		if (!_blocks.contains(action.blockId) || !action.rect.right)
+			continue;
+
+		CDToonsBlock &block = _blocks[action.blockId];
+		if (block.size < 14)
+			error("CDToons: image block %d was too small", action.blockId);
+		// Offsets are relative to the image payload at block-record offset 0x0E.
+		// (0x02) Encoded image width.
+		uint16 width = READ_BE_UINT16(block.data + 2);
+		// (0x00) Encoded image height.
+		uint16 height = READ_BE_UINT16(block.data);
+
+		// (0x0E) Scanline RLE begins after the fixed image header.
+		renderBlock(surface, block.data + 14, block.size - 14,
+			action.rect.left, action.rect.top, width, height, clipRect);
+	}
+}
+
+void CDToonsDecoder::renderBlock(Graphics::Surface &surface, byte *data, uint dataSize, int destX, int destY, uint width, uint height, const Common::Rect *clipRect) {
 	byte *currData = data;
 	byte *dataEnd = data + dataSize;
 
 	debugN(9, "CDToons renderBlock at (%d, %d), width %d, height %d\n",
 		destX, destY, width, height);
 
-	if (destX + (int)width > _surface->w)
-		width = _surface->w - destX;
-	if (destY + (int)height > _surface->h)
-		height = _surface->h - destY;
-
-	uint skip = 0;
-	if (destX < 0) {
-		skip = -destX;
-		if (width <= skip)
-			return;
-		width -= skip;
-		destX = 0;
-	}
+	Common::Rect drawRect(destX, destY, destX + static_cast<int>(width), destY + static_cast<int>(height));
+	drawRect.clip(surface.getRect());
+	if (clipRect)
+		drawRect.clip(*clipRect);
+	if (drawRect.isEmpty())
+		return;
 
-	for (uint y = 0; y < height; y++) {
-		if (destY + (int)y >= _surface->h)
-			break;
+	uint leftToSkipInitial = drawRect.left - destX;
+	uint drawWidth = drawRect.width();
 
+	for (uint y = 0; y < height; y++) {
 		if (currData + 2 > dataEnd)
 			error("CDToons renderBlock overran whole data by %d bytes", (uint32)(currData - dataEnd));
 
+		// Offsets are relative to this scanline entry.
+		// (0x00) Byte length of all RLE runs in this scanline.
 		uint16 lineSize = READ_BE_UINT16(currData);
 		currData += 2;
+		// (0x02) RLE run entries begin at scanline offset.
 		byte *nextLine = currData + lineSize;
 
 		if (nextLine > dataEnd)
 			error("CDToons renderBlock was going to overrun data by %d bytes (line size %d)",
 				(uint32)(nextLine - dataEnd), (uint32)(nextLine - currData));
 
-		if (destY + (int)y < 0) {
+		int outputY = destY + static_cast<int>(y);
+		if (outputY < drawRect.top || drawRect.bottom <= outputY) {
 			currData = nextLine;
 			continue;
 		}
 
-		byte *pixels = (byte *)_surface->getBasePtr(destX, destY + y);
+		byte *pixels = (byte *)surface.getBasePtr(drawRect.left, outputY);
 
-		int leftToSkip = skip;
+		uint leftToSkip = leftToSkipInitial;
 		uint x = 0;
 		bool done = false;
-		while (x < width && !done) {
-			int size = (uint)*currData;
+		while (x < drawWidth && !done) {
+			if (nextLine <= currData)
+				error("CDToons renderBlock reached the end of a line before its pixels");
+			// Offsets are relative to this variable-sized run entry.
+			// (0x00) Run mode and encoded length.
+			uint runSize = *currData;
 			currData++;
-			bool raw = !(size & 0x80);
-			size = (size & 0x7f) + 1;
+			bool raw = !(runSize & 0x80);
+			runSize = (runSize & 0x7f) + 1;
 
 			if (leftToSkip) {
-				if (leftToSkip >= size) {
-					leftToSkip -= size;
+				if (runSize <= leftToSkip) {
+					leftToSkip -= runSize;
 					if (raw)
-						currData += size;
+						currData += runSize;
 					else
 						currData++;
 					continue;
 				} else {
-					size -= leftToSkip;
+					runSize -= leftToSkip;
 					if (raw)
 						currData += leftToSkip;
 					leftToSkip = 0;
 				}
 			}
 
-			if (x + size >= width) {
-				size = width - x;
-				done = true;
-			}
-			if (destX + (int)x + size >= (int)_surface->w) {
-				size = MIN<int>((int)_surface->w - destX - (int)x, width - x);
-				done = true;
-			}
-			if (size <= 0) {
-				size = 0;
+			if (drawWidth <= x + runSize) {
+				runSize = drawWidth - x;
 				done = true;
 			}
 
 			if (raw) {
-				memcpy(pixels + x, currData, size);
-				currData += size;
-				x += size;
+				// (0x01) Literal pixel bytes begin immediately after the run header.
+				memcpy(pixels + x, currData, runSize);
+				currData += runSize;
+				x += runSize;
 			} else {
+				// (0x01) Repeated palette index.
 				byte color = *currData;
 				currData++;
 				if (color) {
-					memset(pixels + x, color, size);
+					memset(pixels + x, color, runSize);
 				}
-				x += size;
+				x += runSize;
 			}
 
 			if (currData > nextLine) {
diff --git a/image/codecs/cdtoons.h b/image/codecs/cdtoons.h
index 2a3169a950c..3ab1e5e98d4 100644
--- a/image/codecs/cdtoons.h
+++ b/image/codecs/cdtoons.h
@@ -24,17 +24,18 @@
 
 #include "image/codecs/codec.h"
 
+#include "common/array.h"
 #include "common/hashmap.h"
+#include "common/rect.h"
 #include "graphics/palette.h"
 
 namespace Image {
 
 struct CDToonsBlock {
-	uint16 flags;
+	uint16 typeFlags;
 	uint32 size;
 	uint16 startFrame;
 	uint16 endFrame;
-	uint16 unknown12;
 	byte *data;
 };
 
@@ -55,14 +56,22 @@ public:
 	bool hasDirtyPalette() const override { return _dirtyPalette; }
 
 private:
+	struct CDToonsAction {
+		uint16 blockId;
+		Common::Rect rect;
+	};
+
 	Graphics::Surface *_surface;
+	Graphics::Surface *_backingSurface;
+	bool _backingSurfaceValid;
 	Graphics::Palette _palette;
 	bool _dirtyPalette;
 	uint16 _currentPaletteId;
 
 	Common::HashMap<uint16, CDToonsBlock> _blocks;
 
-	void renderBlock(byte *data, uint size, int x, int y, uint width, uint height);
+	void renderActions(const Common::Array<CDToonsAction> &actions, uint begin, uint end, Graphics::Surface &surface, const Common::Rect *clipRect);
+	void renderBlock(Graphics::Surface &surface, byte *data, uint size, int x, int y, uint width, uint height, const Common::Rect *clipRect = nullptr);
 	void setPalette(byte *data);
 };
 
diff --git a/test/image/cdtoons.h b/test/image/cdtoons.h
new file mode 100644
index 00000000000..a4bc96b32ea
--- /dev/null
+++ b/test/image/cdtoons.h
@@ -0,0 +1,200 @@
+/* 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 <cxxtest/TestSuite.h>
+
+#if defined(HAVE_CONFIG_H)
+#include "config.h"
+#endif
+
+#include "common/memstream.h"
+#include "graphics/surface.h"
+#include "image/codecs/cdtoons.h"
+
+class CDToonsDecoderTestSuite : public CxxTest::TestSuite {
+public:
+	void test_diff_entry_uses_its_declared_bounds() {
+#ifdef USE_CDTOONS
+		const byte frame[] = {
+			// Header with no cached blocks or actions and a full-surface dirty area.
+			0x00, 0x09, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c,
+			0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			// One 24-byte Diff entry: rectangle, size, dimensions, reserved words,
+			// and a two-pixel repeated RLE run.
+			'D', 'i', 'f', 'f', 0x00, 0x00, 0x00, 0x2a,
+			0x00, 0x01,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02,
+			0x00, 0x00, 0x00, 0x18,
+			0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x02, 0x81, 0x33
+		};
+
+		Image::CDToonsDecoder decoder(2, 1);
+		Common::MemoryReadStream stream(frame, sizeof(frame));
+		const Graphics::Surface *surface = decoder.decodeFrame(stream);
+
+		TS_ASSERT(surface != nullptr);
+		TS_ASSERT_EQUALS(stream.pos(), static_cast<int64>(sizeof(frame)));
+		if (!surface)
+			return;
+
+		const byte *pixels = static_cast<const byte *>(surface->getPixels());
+		TS_ASSERT_EQUALS(pixels[0], 0x33);
+		TS_ASSERT_EQUALS(pixels[1], 0x33);
+#endif
+	}
+
+	void test_decode_frame_with_ignored_marker() {
+#ifdef USE_CDTOONS
+		const byte frame[] = {
+			// Frame header with one background-clearing action.
+			0x00, 0x09, 0x00, 0x01, 0x00, 0x01, 0x00, 0x2a,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02,
+			0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x36,
+			0x00, 0x00, 0x01, 0x0a, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			// Clear with the frame background color, covering the 2x2 surface.
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
+			0x00, 0x02,
+			// Exact opaque, non-rendering Mrks entry shape from a real stream.
+			'M', 'r', 'k', 's', 0x00, 0x00, 0x00, 0x22, 0x00, 0x01,
+			0xe6, 0x68, 0xff, 0xff, 0x00, 0x00, 0x00, 0x18,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x69, 0x53,
+			0x74, 0x61, 0x02, 0x3e, 0xe3, 0xe0, 0x6b, 0x00
+		};
+
+		Image::CDToonsDecoder decoder(2, 2);
+		Common::MemoryReadStream stream(frame, sizeof(frame));
+		const Graphics::Surface *surface = decoder.decodeFrame(stream);
+
+		TS_ASSERT(surface != nullptr);
+		TS_ASSERT_EQUALS(stream.pos(), static_cast<int64>(sizeof(frame)));
+		if (!surface)
+			return;
+
+		const byte *pixels = static_cast<const byte *>(surface->getPixels());
+		for (uint y = 0; y < 2; y++) {
+			for (uint x = 0; x < 2; x++)
+				TS_ASSERT_EQUALS(pixels[y * surface->pitch + x], 0x2a);
+		}
+#endif
+	}
+
+	void test_xfrm_retains_background_action_range() {
+#ifdef USE_CDTOONS
+		const byte firstFrame[] = {
+			// Header: two image blocks and three actions, with a full-frame dirty rectangle.
+			0x00, 0x09, 0x00, 0x01, 0x00, 0x01, 0x00, 0x05,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02,
+			0x00, 0x00, 0x00, 0x10, 0x00, 0x02, 0x00, 0x4a,
+			0x00, 0x02, 0x03, 0x0a, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			// Action 0 is empty. Action 1 is the retained background; action 2 is foreground.
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00,
+			0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+			0x00, 0x02,
+			0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01,
+			0x00, 0x02,
+			// Block 1: a two-pixel background row containing color 0x11.
+			0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21,
+			0x00, 0x01, 0xff, 0xff, 0x01, 0x00,
+			0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x03, 0x01, 0x11, 0x11,
+			// Block 2: a one-pixel foreground row containing color 0x22.
+			0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20,
+			0x00, 0x01, 0xff, 0xff, 0x02, 0x00,
+			0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x02, 0x00, 0x22,
+			// XFrm selects action 1 (encoded start 2, count 1) for the backing layer.
+			'X', 'F', 'r', 'm', 0x00, 0x00, 0x00, 0x1a,
+			0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+			0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00
+		};
+		const byte secondFrame[] = {
+			// The next frame performs a transparent background update without clearing.
+			0x00, 0x09, 0x00, 0x02, 0x00, 0x02, 0x00, 0x05,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02,
+			0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x4a,
+			0x00, 0x03, 0x03, 0x0a, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00,
+			0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+			0x00, 0x02,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00,
+			// Block 3: two transparent pixels.
+			0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20,
+			0x00, 0x02, 0xff, 0xff, 0x01, 0x00,
+			0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x02, 0x81, 0x00,
+			'X', 'F', 'r', 'm', 0x00, 0x00, 0x00, 0x1a,
+			0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+			0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00,
+			'B', 'c', 'k', 'R', 0x00, 0x00, 0x00, 0x12,
+			0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+			0x00, 0x02,
+			// Opaque extension chunks are accepted by size and do not affect composition.
+			'S', 'c', 'a', 'l', 0x00, 0x00, 0x00, 0x0c,
+			0xde, 0xad, 0xbe, 0xef,
+			'W', 'r', 'M', 'p', 0x00, 0x00, 0x00, 0x0a,
+			0xaa, 0x55
+		};
+
+		Image::CDToonsDecoder decoder(2, 1);
+		Common::MemoryReadStream firstStream(firstFrame, sizeof(firstFrame));
+		const Graphics::Surface *surface = decoder.decodeFrame(firstStream);
+		TS_ASSERT(surface != nullptr);
+		TS_ASSERT_EQUALS(firstStream.pos(), static_cast<int64>(sizeof(firstFrame)));
+		if (!surface)
+			return;
+
+		const byte *pixels = static_cast<const byte *>(surface->getPixels());
+		TS_ASSERT_EQUALS(pixels[0], 0x11);
+		TS_ASSERT_EQUALS(pixels[1], 0x22);
+
+		Common::MemoryReadStream secondStream(secondFrame, sizeof(secondFrame));
+		surface = decoder.decodeFrame(secondStream);
+		TS_ASSERT(surface != nullptr);
+		TS_ASSERT_EQUALS(secondStream.pos(), static_cast<int64>(sizeof(secondFrame)));
+		if (!surface)
+			return;
+
+		pixels = static_cast<const byte *>(surface->getPixels());
+		TS_ASSERT_EQUALS(pixels[0], 0x11);
+		TS_ASSERT_EQUALS(pixels[1], 0x11);
+#endif
+	}
+};




More information about the Scummvm-git-logs mailing list