/* 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * $URL: https://scummvm.svn.sourceforge.net/svnroot/scummvm/scummvm/tags/release-0-12-0/engines/scumm/gfx.cpp $
 * $Id: gfx.cpp 33547 2008-08-02 21:58:39Z sev $
 *
 */


#include "common/system.h"
#include "scumm/scumm.h"
#include "scumm/actor.h"
#include "scumm/charset.h"
#include "scumm/intern.h"
#ifdef ENABLE_HE
#include "scumm/he/intern_he.h"
#endif
#include "scumm/resource.h"
#include "scumm/usage_bits.h"
#include "scumm/he/wiz_he.h"
#include "scumm/util.h"

#ifdef USE_ARM_GFX_ASM
extern "C" void asmDrawStripToScreen(int height, int width, byte const* text, byte const* src, byte* dst,
	int vsPitch, int vmScreenWidth, int textSurfacePitch);
extern "C" void asmCopy8Col(byte* dst, int dstPitch, const byte* src, int height);
#endif /* USE_ARM_GFX_ASM */

namespace Scumm {

static void blit(byte *dst, int dstPitch, const byte *src, int srcPitch, int w, int h);
static void fill(byte *dst, int dstPitch, byte color, int w, int h);
#ifndef ARM_USE_GFX_ASM
static void copy8Col(byte *dst, int dstPitch, const byte *src, int height);
#endif
static void clear8Col(byte *dst, int dstPitch, int height);

static void ditherHerc(byte *src, byte *hercbuf, int srcPitch, int *x, int *y, int *width, int *height);
static void scale2x(byte *dst, int dstPitch, const byte *src, int srcPitch, int w, int h);

struct StripTable {
	int offsets[160];
	int run[160];
	int color[160];
	int zoffsets[120];	// FIXME: Why only 120 here?
	int zrun[120];		// FIXME: Why only 120 here?
};

enum {
	kScrolltime = 500,  // ms scrolling is supposed to take
	kPictureDelay = 20,
	kFadeDelay = 4 // 1/4th of a jiffie
};

#define NUM_SHAKE_POSITIONS 8
static const int8 shake_positions[NUM_SHAKE_POSITIONS] = {
	0, 1 * 2, 2 * 2, 1 * 2, 0 * 2, 2 * 2, 3 * 2, 1 * 2
};

/**
 * The following structs define four basic fades/transitions used by
 * transitionEffect(), each looking differently to the user.
 * Note that the stripTables contain strip numbers, and they assume
 * that the screen has 40 vertical strips (i.e. 320 pixel), and 25 horizontal
 * strips (i.e. 200 pixel). There is a hack in transitionEffect that
 * makes it work correctly in games which have a different screen height
 * (for example, 240 pixel), but nothing is done regarding the width, so this
 * code won't work correctly in COMI. Also, the number of iteration depends
 * on min(vertStrips, horizStrips}. So the 13 is derived from 25/2, rounded up.
 * And the 25 = min(25,40). Hence for Zak256 instead of 13 and 25, the values
 * 15 and 30 should be used, and for COMI probably 30 and 60.
 */
struct TransitionEffect {
	byte numOfIterations;
	int8 deltaTable[16];	// four times l / t / r / b
	byte stripTable[16];	// ditto
};

static const TransitionEffect transitionEffects[6] = {
	// Iris effect (looks like an opening/closing camera iris)
	{
		13,		// Number of iterations
		{
			1,  1, -1,  1,
		   -1,  1, -1, -1,
			1, -1, -1, -1,
			1,  1,  1, -1
		},
		{
			0,  0, 39,  0,
		   39,  0, 39, 24,
			0, 24, 39, 24,
			0,  0,  0, 24
		}
	},

	// Box wipe (a box expands from the upper-left corner to the lower-right corner)
	{
		25,		// Number of iterations
		{
			0,  1,  2,  1,
			2,  0,  2,  1,
			2,  0,  2,  1,
			0,  0,  0,  0
		},
		{
			0,  0,  0,  0,
			0,  0,  0,  0,
			1,  0,  1,  0,
		  255,  0,  0,  0
		}
	},

	// Box wipe (a box expands from the lower-right corner to the upper-left corner)
	{
		25,		// Number of iterations
		{
		   -2, -1,  0, -1,
		   -2, -1, -2,  0,
		   -2, -1, -2,  0,
			0,  0,  0,  0
		},
		{
		   39, 24, 39, 24,
		   39, 24, 39, 24,
		   38, 24, 38, 24,
		  255,  0,  0,  0
		}
	},

	// Inverse box wipe
	{
		25,		// Number of iterations
		{
			0, -1, -2, -1,
		   -2,  0, -2, -1,
		   -2,  0, -2, -1,
		    0,  0,  0,  0
		},
		{
			0, 24, 39, 24,
		   39,  0, 39, 24,
		   38,  0, 38, 24,
		  255,  0,  0,  0
		}
	},

	// Inverse iris effect, specially tailored for V1/V2 games
	{
		9,		// Number of iterations
		{
			-1, -1,  1, -1,
			-1,  1,  1,  1,
			-1, -1, -1,  1,
			 1, -1,  1,  1
		},
		{
			 7, 7, 32, 7,
			 7, 8, 32, 8,
			 7, 8,  7, 8,
			32, 7, 32, 8
		}
	},

	// Horizontal wipe (a box expands from left to right side). For MM NES
	{
		16,		// Number of iterations
		{
			  2,  0,  2,  0,
			  2,  0,  2,  0,
			  0,  0,  0,  0,
			  0,  0,  0,  0
		},
		{
			  0, 0,  0,  15,
			  1, 0,  1,  15,
			255, 0,  0,  0,
			255, 0,  0,  0
		}
	}

};


Gdi::Gdi(ScummEngine *vm) : _vm(vm) {
	_numZBuffer = 0;
	memset(_imgBufOffs, 0, sizeof(_imgBufOffs));
	_numStrips = 0;

	_paletteMod = 0;
	_roomPalette = vm->_roomPalette;
	_transparentColor = 255;
	_decomp_shr = 0;
	_decomp_mask = 0;
	_vertStripNextInc = 0;
	_zbufferDisabled = false;
	_objectMode = false;
}

Gdi::~Gdi() {
}

GdiNES::GdiNES(ScummEngine *vm) : Gdi(vm) {
	memset(&_NES, 0, sizeof(_NES));
}

GdiV1::GdiV1(ScummEngine *vm) : Gdi(vm) {
	memset(&_C64, 0, sizeof(_C64));
}

GdiV2::GdiV2(ScummEngine *vm) : Gdi(vm) {
	_roomStrips = 0;
}

GdiV2::~GdiV2() {
	free(_roomStrips);
}

void Gdi::init() {
	_numStrips = _vm->_screenWidth / 8;

	// Increase the number of screen strips by one; needed for smooth scrolling
	if (_vm->_game.version >= 7) {
		// We now have mostly working smooth scrolling code in place for V7+ games
		// (i.e. The Dig, Full Throttle and COMI). It seems to work very well so far.
		//
		// To understand how we achieve smooth scrolling, first note that with it, the
		// virtual screen strips don't match the display screen strips anymore. To
		// overcome that problem, we simply use a screen pitch that is 8 pixel wider
		// than the actual screen width, and always draw one strip more than needed to
		// the backbuf (thus we have to treat the right border seperately).
		_numStrips += 1;
	}
}

void Gdi::roomChanged(byte *roomptr) {
}

void GdiNES::roomChanged(byte *roomptr) {
	decodeNESGfx(roomptr);
}

void GdiV1::roomChanged(byte *roomptr) {
	for (int i = 0; i < 4; i++){
		_C64.colors[i] = roomptr[6 + i];
	}
	decodeC64Gfx(roomptr + READ_LE_UINT16(roomptr + 10), _C64.charMap, 2048);
	decodeC64Gfx(roomptr + READ_LE_UINT16(roomptr + 12), _C64.picMap, roomptr[4] * roomptr[5]);
	decodeC64Gfx(roomptr + READ_LE_UINT16(roomptr + 14), _C64.colorMap, roomptr[4] * roomptr[5]);
	decodeC64Gfx(roomptr + READ_LE_UINT16(roomptr + 16), _C64.maskMap, roomptr[4] * roomptr[5]);

	// Read the mask data. The 16bit length value seems to always be 8 too big.
	// See bug #1837375 for details on this.
	const byte *maskPtr = roomptr + READ_LE_UINT16(roomptr + 18);
	decodeC64Gfx(maskPtr + 2, _C64.maskChar, READ_LE_UINT16(maskPtr) - 8);
	_objectMode = true;
}

void GdiV2::roomChanged(byte *roomptr) {
	_roomStrips = generateStripTable(roomptr + READ_LE_UINT16(roomptr + 0x0A),
			_vm->_roomWidth, _vm->_roomHeight, _roomStrips);
}

#pragma mark -
#pragma mark --- Virtual Screens ---
#pragma mark -


void ScummEngine::initScreens(int b, int h) {
	int i;
	int adj = 0;

	for (i = 0; i < 3; i++) {
		_res->nukeResource(rtBuffer, i + 1);
		_res->nukeResource(rtBuffer, i + 5);
	}

	if (!getResourceAddress(rtBuffer, 4)) {
		// Since the size of screen 3 is fixed, there is no need to reallocate
		// it if its size changed.
		// Not sure what it is good for, though. I think it may have been used
		// in pre-V7 for the games messages (like 'Pause', Yes/No dialogs,
		// version display, etc.). I don't know about V7, maybe the same is the
		// case there. If so, we could probably just remove it completely.
		if (_game.version >= 7) {
			initVirtScreen(kUnkVirtScreen, (_screenHeight / 2) - 10, _screenWidth, 13, false, false);
		} else {
			initVirtScreen(kUnkVirtScreen, 80, _screenWidth, 13, false, false);
		}
	}

	if ((_game.platform == Common::kPlatformNES) && (h != _screenHeight)) {
		// This is a hack to shift the whole screen downwards to match the original.
		// Otherwise we would have to do lots of coordinate adjustments all over
		// the code.
		adj = 16;
		initVirtScreen(kUnkVirtScreen, 0, _screenWidth, adj, false, false);
	}

	initVirtScreen(kMainVirtScreen, b + adj, _screenWidth, h - b, true, true);
	initVirtScreen(kTextVirtScreen, adj, _screenWidth, b, false, false);
	initVirtScreen(kVerbVirtScreen, h + adj, _screenWidth, _screenHeight - h - adj, false, false);
	_screenB = b;
	_screenH = h;

	_gdi->init();
}

void ScummEngine::initVirtScreen(VirtScreenNumber slot, int top, int width, int height, bool twobufs,
													 bool scrollable) {
	VirtScreen *vs = &_virtscr[slot];
	int size;

	assert(height >= 0);
	assert(slot >= 0 && slot < 4);

	if (_game.version >= 7) {
		if (slot == kMainVirtScreen && (_roomHeight != 0))
			height = _roomHeight;
	}

	vs->number = slot;
	vs->w = width;
	vs->topline = top;
	vs->h = height;
	vs->hasTwoBuffers = twobufs;
	vs->xstart = 0;
	vs->backBuf = NULL;
	vs->bytesPerPixel = 1;
	vs->pitch = width;

	if (_game.version >= 7) {
		// Increase the pitch by one; needed to accomodate the extra screen
		// strip which we use to implement smooth scrolling. See Gdi::init().
		vs->pitch += 8;
	}

	size = vs->pitch * vs->h;
	if (scrollable) {
		// Allow enough spaces so that rooms can be up to 4 resp. 8 screens
		// wide. To achieve horizontal scrolling, SCUMM uses a neat trick:
		// only the offset into the screen buffer (xstart) is changed. That way
		// very little of the screen has to be redrawn, and we have a very low
		// memory overhead (namely for every pixel we want to scroll, we need
		// one additional byte in the buffer).
		if (_game.version >= 7) {
			size += vs->pitch * 8;
		} else {
			size += vs->pitch * 4;
		}
	}

	_res->createResource(rtBuffer, slot + 1, size);
	vs->pixels = getResourceAddress(rtBuffer, slot + 1);
	memset(vs->pixels, 0, size);	// reset background

	if (twobufs) {
		vs->backBuf = _res->createResource(rtBuffer, slot + 5, size);
	}

	if (slot != 3) {
		vs->setDirtyRange(0, height);
	}
}

VirtScreen *ScummEngine::findVirtScreen(int y) {
	VirtScreen *vs = _virtscr;
	int i;

	for (i = 0; i < 3; i++, vs++) {
		if (y >= vs->topline && y < vs->topline + vs->h) {
			return vs;
		}
	}
	return NULL;
}

void ScummEngine::markRectAsDirty(VirtScreenNumber virt, int left, int right, int top, int bottom, int dirtybit) {
	VirtScreen *vs = &_virtscr[virt];
	int lp, rp;

	if (left > right || top > bottom)
		return;
	if (top > vs->h || bottom < 0)
		return;

	if (top < 0)
		top = 0;
	if (bottom > vs->h)
		bottom = vs->h;

	if (virt == kMainVirtScreen && dirtybit) {

		lp = left / 8 + _screenStartStrip;
		if (lp < 0)
			lp = 0;

		rp = (right + vs->xstart) / 8;
		if (_game.version >= 7) {
			if (rp > 409)
				rp = 409;
		} else {
			if (rp >= 200)
				rp = 200;
		}
		for (; lp <= rp; lp++)
			setGfxUsageBit(lp, dirtybit);
	}

	// The following code used to be in the separate method setVirtscreenDirty
	lp = left / 8;
	rp = right / 8;

	if ((lp >= _gdi->_numStrips) || (rp < 0))
		return;
	if (lp < 0)
		lp = 0;
	if (rp >= _gdi->_numStrips)
		rp = _gdi->_numStrips - 1;

	while (lp <= rp) {
		if (top < vs->tdirty[lp])
			vs->tdirty[lp] = top;
		if (bottom > vs->bdirty[lp])
			vs->bdirty[lp] = bottom;
		lp++;
	}
}

/**
 * Update all dirty screen areas. This method blits all of the internal engine
 * graphics to the actual display, as needed. In addition, the 'shaking'
 * code in the backend is controlled from here.
 */
void ScummEngine::drawDirtyScreenParts() {
	// Update verbs
	updateDirtyScreen(kVerbVirtScreen);

	// Update the conversation area (at the top of the screen)
	updateDirtyScreen(kTextVirtScreen);

	// Update game area ("stage")
	if (camera._last.x != camera._cur.x || (_game.version >= 7 && (camera._cur.y != camera._last.y))) {
		// Camera moved: redraw everything
		VirtScreen *vs = &_virtscr[kMainVirtScreen];
		drawStripToScreen(vs, 0, vs->w, 0, vs->h);
		vs->setDirtyRange(vs->h, 0);
	} else {
		updateDirtyScreen(kMainVirtScreen);
	}

	// Handle shaking
	if (_shakeEnabled) {
		_shakeFrame = (_shakeFrame + 1) % NUM_SHAKE_POSITIONS;
		_system->setShakePos(shake_positions[_shakeFrame]);
	} else if (!_shakeEnabled &&_shakeFrame != 0) {
		_shakeFrame = 0;
		_system->setShakePos(0);
	}
}

void ScummEngine_v6::drawDirtyScreenParts() {
	// For the Full Throttle credits to work properly, the blast
	// texts have to be drawn before the blast objects. Unless
	// someone can think of a better way to achieve this effect.

	if (_game.version >= 7 && VAR(VAR_BLAST_ABOVE_TEXT) == 1) {
		drawBlastTexts();
		drawBlastObjects();
		if (_game.version == 8) {
			// Does this case ever happen? We need to draw the
			// actor over the blast object, so we're forced to
			// also draw it over the subtitles.
			processUpperActors();
		}
	} else {
		drawBlastObjects();
		if (_game.version == 8) {
			// Do this before drawing blast texts. Subtitles go on
			// top of the CoMI verb coin, e.g. when Murray is
			// talking to himself early in the game.
			processUpperActors();
		}
		drawBlastTexts();
	}

	// Call the original method.
	ScummEngine::drawDirtyScreenParts();

	// Remove all blasted objects/text again.
	removeBlastTexts();
	removeBlastObjects();
}

/**
 * Blit the dirty data from the given VirtScreen to the display. If the camera moved,
 * a full blit is done, otherwise only the visible dirty areas are updated.
 */
void ScummEngine::updateDirtyScreen(VirtScreenNumber slot) {
	VirtScreen *vs = &_virtscr[slot];

	// Do nothing for unused virtual screens
	if (vs->h == 0)
		return;

	int i;
	int w = 8;
	int start = 0;

	for (i = 0; i < _gdi->_numStrips; i++) {
		if (vs->bdirty[i]) {
			const int top = vs->tdirty[i];
			const int bottom = vs->bdirty[i];
			vs->tdirty[i] = vs->h;
			vs->bdirty[i] = 0;
			if (i != (_gdi->_numStrips - 1) && vs->bdirty[i + 1] == bottom && vs->tdirty[i + 1] == top) {
				// Simple optimizations: if two or more neighbouring strips
				// form one bigger rectangle, coalesce them.
				w += 8;
				continue;
			}
			drawStripToScreen(vs, start * 8, w, top, bottom);
			w = 8;
		}
		start = i + 1;
	}
}

/**
 * Blit the specified rectangle from the given virtual screen to the display.
 * Note: t and b are in *virtual screen* coordinates, while x is relative to
 * the *real screen*. This is due to the way tdirty/vdirty work: they are
 * arrays which map 'strips' (sections of the real screen) to dirty areas as
 * specified by top/bottom coordinate in the virtual screen.
 */
void ScummEngine::drawStripToScreen(VirtScreen *vs, int x, int width, int top, int bottom) {

	// Short-circuit if nothing has to be drawn
	if (bottom <= top || top >= vs->h)
		return;

	// Some paranoia checks
	assert(top >= 0 && bottom <= vs->h);
	assert(x >= 0 && width <= vs->pitch);
	assert(_textSurface.pixels);

	// Perform some clipping
	if (width > vs->w - x)
		width = vs->w - x;
	if (top < _screenTop)
		top = _screenTop;
	if (bottom > _screenTop + _screenHeight)
		bottom = _screenTop + _screenHeight;

	// Convert the vertical coordinates to real screen coords
	int y = vs->topline + top - _screenTop;
	int height = bottom - top;

	if (width <= 0 || height <= 0)
		return;

	const byte *src = vs->getPixels(x, top);
	int m = _textSurfaceMultiplier;
	byte *dst;
	int vsPitch;
	int pitch = vs->pitch;

	if (_useCJKMode && _textSurfaceMultiplier == 2) {
		dst = _fmtownsBuf;

		scale2x(dst, _screenWidth * m, src, vs->pitch,  width, height);
		src = dst;

		vsPitch = _screenWidth * m - width * m;

	} else {
		vsPitch = vs->pitch - width;
	}

	dst = _compositeBuf;

	if (_game.version < 7) {
		// For The Dig, FT and COMI, we just blit everything to the screen at once.
		// For older games, things are more complicated. First off, we need to
		// deal with the _textSurface, which needs to be composited over the
		// screen contents. Secondly, a rendering mode might be active, which
		// means a filter has to be applied.

		// Compute pointer to the text surface
		assert(_compositeBuf);
		const byte *text = (byte *)_textSurface.getBasePtr(x * m, y * m);

		// The values x, width, etc. are all multiples of 8 at this point,
		// so loop unrolloing might be a good idea...
		assert(0 == ((long)text & 3));
		assert(0 == (width & 3));

		// Compose the text over the game graphics

		// TODO: Optimize this code. There are several things that come immediately to mind:
		// (1) Loop unrolling: We could read 4 or even 8 pixels at once, since everything is
		//     a multiple of 8 here.
		// (2) More ASM versions (in particular, the ARM code for the NDS could be used on
		//     all ARM systems, couldn't it?)
		// (3) Better encoding of the text surface data. This is the one with the biggest
		//     potential.
		//     (a) Keep an "isEmpty" marker for each pixel row in the _textSurface. The idea
		//         is that most rows won't contain any text data, so we can just use memcpy.
		//     (b) RLE encode the _textSurface row-wise. This is an improved variant of (a),
		//         but also more complicated to implement, and incurs a bigger overhead when
		//         writing to the text surface.
#ifdef ARM_USE_GFX_ASM
		asmDrawStripToScreen(height, width, text, src, dst, vs->pitch, width, _textSurface.pitch);
#else
		for (int h = 0; h < height * m; ++h) {
			for (int w = 0; w < width * m; ++w) {
				byte tmp = *text++;
				if (tmp == CHARSET_MASK_TRANSPARENCY)
					tmp = *src;
				*dst++ = tmp;
				src++;
			}
			src += vsPitch;
			text += _textSurface.pitch - width * m;
		}
#endif
		src = _compositeBuf;
		pitch = width;

		if (_renderMode == Common::kRenderHercA || _renderMode == Common::kRenderHercG) {
			ditherHerc(_compositeBuf, _herculesBuf, width, &x, &y, &width, &height);

			src = _herculesBuf + x + y * Common::kHercW;
			pitch = Common::kHercW;

			// center image on the screen
			x += (Common::kHercW - _screenWidth * 2) / 2;	// (720 - 320*2)/2 = 40
		} else if (_useCJKMode && m == 2) {
			pitch *= m;
			x *= m;
			y *= m;
			width *= m;
			height *= m;
		} else {
			if (_renderMode == Common::kRenderCGA)
				ditherCGA(_compositeBuf, width, x, y, width, height);

			// HACK: This is dirty hack which renders narrow NES rooms centered
			// NES can address negative number strips and that poses problem for
			// our code. So instead of adding zillions of fixes and potentially
			// breaking other games, we shift it right at the rendering stage.
			if ((_game.platform == Common::kPlatformNES) && (((_NESStartStrip > 0) && (vs->number == kMainVirtScreen)) || (vs->number == kTextVirtScreen))) {
				x += 16;
				while (x + width >= _screenWidth)
					width -= 16;
				if (width <= 0)
					return;
			}

		}
	}

	// Finally blit the whole thing to the screen
	_system->copyRectToScreen(src, pitch, x, y, width, height);
}

// CGA
// indy3 loom maniac monkey1 zak
//
// Herc (720x350)
// maniac monkey1 zak
//
// EGA
// monkey2 loom maniac monkey1 atlantis indy3 zak loomcd

static const byte cgaDither[2][2][16] = {
	{{0, 1, 0, 1, 2, 2, 0, 0, 3, 1, 3, 1, 3, 2, 1, 3},
	 {0, 0, 1, 1, 0, 2, 2, 3, 0, 3, 1, 1, 3, 3, 1, 3}},
	{{0, 0, 1, 1, 0, 2, 2, 3, 0, 3, 1, 1, 3, 3, 1, 3},
	 {0, 1, 0, 1, 2, 2, 0, 0, 3, 1, 1, 1, 3, 2, 1, 3}}};

// CGA dithers 4x4 square with direct substitutes
// Odd lines have colors swapped, so there will be checkered patterns.
// But apparently there is a mistake for 10th color.
void ScummEngine::ditherCGA(byte *dst, int dstPitch, int x, int y, int width, int height) const {
	byte *ptr;
	int idx1, idx2;

	for (int y1 = 0; y1 < height; y1++) {
		ptr = dst + y1 * dstPitch;

		if (_game.version == 2)
			idx1 = 0;
		else
			idx1 = (y + y1) % 2;

		for (int x1 = 0; x1 < width; x1++) {
			idx2 = (x + x1) % 2;
			*ptr = cgaDither[idx1][idx2][*ptr & 0xF];
			ptr++;
		}
	}
}

// Hercules dithering. It uses same dithering tables but output is 1bpp and
// it stretches in this way:
//         aaaa0
// aa      aaaa1
// bb      bbbb0      Here 0 and 1 mean dithering table row number
// cc -->  bbbb1
// dd      cccc0
//         cccc1
//         dddd0
void ditherHerc(byte *src, byte *hercbuf, int srcPitch, int *x, int *y, int *width, int *height) {
	byte *srcptr, *dstptr;
	const int xo = *x, yo = *y, widtho = *width, heighto = *height;
	int dsty = yo*2 - yo/4;

	for (int y1 = 0; y1 < heighto;) {
		assert(dsty < Common::kHercH);

		srcptr = src + y1 * srcPitch;
		dstptr = hercbuf + dsty * Common::kHercW + xo * 2;

		const int idx1 = (dsty % 7) % 2;
		for (int x1 = 0; x1 < widtho; x1++) {
			const int idx2 = (xo + x1) % 2;
			const byte tmp = cgaDither[idx1][idx2][*srcptr & 0xF];
			*dstptr++ = tmp >> 1;
			*dstptr++ = tmp & 0x1;
			srcptr++;
		}
		if (idx1 || dsty % 7 == 6)
			y1++;
		dsty++;
	}

	*x *= 2;
	*y = yo*2 - yo/4;
	*width *= 2;
	*height = dsty - *y;
}

void scale2x(byte *dst, int dstPitch, const byte *src, int srcPitch, int w, int h) {
	byte *dstL1 = dst;
	byte *dstL2 = dst + dstPitch;

	int dstAdd = dstPitch * 2 - w * 2;
	int srcAdd = srcPitch - w;

	while (h--) {
		for (int x = 0; x < w; ++x, dstL1 += 2, dstL2 += 2) {
			uint16 col = *src++;
			col |= col << 8;
			*(uint16*)(dstL1) = col;
			*(uint16*)(dstL2) = col;
		}
		dstL1 += dstAdd; dstL2 += dstAdd;
		src += srcAdd;
	}
}


#pragma mark -
#pragma mark --- Background buffers & charset mask ---
#pragma mark -


void ScummEngine::initBGBuffers(int height) {
	const byte *ptr;
	int size, itemsize, i;
	byte *room;

	if (_game.version >= 7) {
		// Resize main virtual screen in V7 games. This is necessary
		// because in V7, rooms may be higher than one screen, so we have
		// to accomodate for that.
		initVirtScreen(kMainVirtScreen, _virtscr[kMainVirtScreen].topline, _screenWidth, height, true, true);
	}

	if (_game.heversion >= 70)
		room = getResourceAddress(rtRoomImage, _roomResource);
	else
		room = getResourceAddress(rtRoom, _roomResource);

	if (_game.version <= 3) {
		_gdi->_numZBuffer = 2;
	} else if (_game.features & GF_SMALL_HEADER) {
		int off;
		ptr = findResourceData(MKID_BE('SMAP'), room);
		_gdi->_numZBuffer = 0;

		if (_game.features & GF_16COLOR)
			off = READ_LE_UINT16(ptr);
		else
			off = READ_LE_UINT32(ptr);

		while (off && _gdi->_numZBuffer < 4) {
			_gdi->_numZBuffer++;
			ptr += off;
			off = READ_LE_UINT16(ptr);
		}
	} else if (_game.version == 8) {
		// in V8 there is no RMIH and num z buffers is in RMHD
		ptr = findResource(MKID_BE('RMHD'), room);
		_gdi->_numZBuffer = READ_LE_UINT32(ptr + 24) + 1;
	} else if (_game.heversion >= 70) {
		ptr = findResource(MKID_BE('RMIH'), room);
		_gdi->_numZBuffer = READ_LE_UINT16(ptr + 8) + 1;
	} else {
		ptr = findResource(MKID_BE('RMIH'), findResource(MKID_BE('RMIM'), room));
		_gdi->_numZBuffer = READ_LE_UINT16(ptr + 8) + 1;
	}
	assert(_gdi->_numZBuffer >= 1 && _gdi->_numZBuffer <= 8);

	if (_game.version >= 7)
		itemsize = (_roomHeight + 10) * _gdi->_numStrips;
	else
		itemsize = (_roomHeight + 4) * _gdi->_numStrips;


	size = itemsize * _gdi->_numZBuffer;
	memset(_res->createResource(rtBuffer, 9, size), 0, size);

	for (i = 0; i < (int)ARRAYSIZE(_gdi->_imgBufOffs); i++) {
		if (i < _gdi->_numZBuffer)
			_gdi->_imgBufOffs[i] = i * itemsize;
		else
			_gdi->_imgBufOffs[i] = (_gdi->_numZBuffer - 1) * itemsize;
	}
}

/**
 * Redraw background as needed, i.e. the left/right sides if scrolling took place etc.
 * Note that this only updated the virtual screen, not the actual display.
 */
void ScummEngine::redrawBGAreas() {
	int i;
	int diff;
	int val = 0;

	if (_game.id != GID_PASS && _game.version >= 4 && _game.version <= 6) {
		// Starting with V4 games (with the exception of the PASS demo), text
		// is drawn over the game graphics (as  opposed to be drawn in a
		// separate region of the screen). So, when scrolling in one of these
		// games (pre-new camera system), if actor text is visible (as indicated
		// by the _hasMask flag), we first remove it before proceeding.
		if (camera._cur.x != camera._last.x && _charset->_hasMask)
			stopTalk();
	}

	// Redraw parts of the background which are marked as dirty.
	if (!_fullRedraw && _bgNeedsRedraw) {
		for (i = 0; i != _gdi->_numStrips; i++) {
			if (testGfxUsageBit(_screenStartStrip + i, USAGE_BIT_DIRTY)) {
				redrawBGStrip(i, 1);
			}
		}
	}

	if (_game.version >= 7) {
		diff = camera._cur.x / 8 - camera._last.x / 8;
		if (_fullRedraw || ABS(diff) >= _gdi->_numStrips) {
			_bgNeedsRedraw = false;
			redrawBGStrip(0, _gdi->_numStrips);
		} else if (diff > 0) {
			val = -diff;
			redrawBGStrip(_gdi->_numStrips - diff, diff);
		} else if (diff < 0) {
			val = -diff;
			redrawBGStrip(0, -diff);
		}
	} else {
		diff = camera._cur.x - camera._last.x;
		if (!_fullRedraw && diff == 8) {
			val = -1;
			redrawBGStrip(_gdi->_numStrips - 1, 1);
		} else if (!_fullRedraw && diff == -8) {
			val = +1;
			redrawBGStrip(0, 1);
		} else if (_fullRedraw || diff != 0) {
			if (_game.version <= 5) {
				((ScummEngine_v5 *)this)->clearFlashlight();
			}
			_bgNeedsRedraw = false;
			redrawBGStrip(0, _gdi->_numStrips);
		}
	}

	drawRoomObjects(val);
	_bgNeedsRedraw = false;
}

#ifdef ENABLE_HE
void ScummEngine_v71he::redrawBGAreas() {
	if (camera._cur.x != camera._last.x && _charset->_hasMask)
		stopTalk();

	byte *room = getResourceAddress(rtRoomImage, _roomResource) + _IM00_offs;
	if (_fullRedraw) {
		_bgNeedsRedraw = false;
		_gdi->drawBMAPBg(room, &_virtscr[kMainVirtScreen]);
	}

	drawRoomObjects(0);
	_bgNeedsRedraw = false;
}

void ScummEngine_v72he::redrawBGAreas() {
	ScummEngine_v71he::redrawBGAreas();
	_wiz->flushWizBuffer();
}
#endif

void ScummEngine::redrawBGStrip(int start, int num) {
	byte *room;

	int s = _screenStartStrip + start;

	for (int i = 0; i < num; i++)
		setGfxUsageBit(s + i, USAGE_BIT_DIRTY);

	if (_game.heversion >= 70)
		room = getResourceAddress(rtRoomImage, _roomResource);
	else
		room = getResourceAddress(rtRoom, _roomResource);

	_gdi->drawBitmap(room + _IM00_offs, &_virtscr[kMainVirtScreen], s, 0, _roomWidth, _virtscr[kMainVirtScreen].h, s, num, 0);
}

void ScummEngine::restoreBackground(Common::Rect rect, byte backColor) {
	VirtScreen *vs;
	byte *screenBuf;

	if (rect.top < 0)
		rect.top = 0;
	if (rect.left >= rect.right || rect.top >= rect.bottom)
		return;

	if ((vs = findVirtScreen(rect.top)) == NULL)
		return;

	if (rect.left > vs->w)
		return;

	// Convert 'rect' to local (virtual screen) coordinates
	rect.top -= vs->topline;
	rect.bottom -= vs->topline;

	rect.clip(vs->w, vs->h);

	markRectAsDirty(vs->number, rect, USAGE_BIT_RESTORED);

	screenBuf = vs->getPixels(rect.left, rect.top);

	const int height = rect.height();
	const int width = rect.width();

	if (!height)
		return;

	if (vs->hasTwoBuffers && _currentRoom != 0 && isLightOn()) {
		blit(screenBuf, vs->pitch, vs->getBackPixels(rect.left, rect.top), vs->pitch, width, height);
		if (vs->number == kMainVirtScreen && _charset->_hasMask) {
			byte *mask = (byte *)_textSurface.getBasePtr(rect.left, rect.top - _screenTop);
			fill(mask, _textSurface.pitch, CHARSET_MASK_TRANSPARENCY, width, height);
		}
	} else {
		fill(screenBuf, vs->pitch, backColor, width, height);
	}
}

void ScummEngine::restoreCharsetBg() {
	_nextLeft = _string[0].xpos;
	_nextTop = _string[0].ypos + _screenTop;

	if (_charset->_hasMask) {
		_charset->_hasMask = false;
		_charset->_str.left = -1;
		_charset->_left = -1;

		// Restore background on the whole text area. This code is based on
		// restoreBackground(), but was changed to only restore those parts which are
		// currently covered by the charset mask.

		VirtScreen *vs = &_virtscr[_charset->_textScreenID];
		if (!vs->h)
			return;

		markRectAsDirty(vs->number, Common::Rect(vs->w, vs->h), USAGE_BIT_RESTORED);

		byte *screenBuf = vs->getPixels(0, 0);

		if (vs->hasTwoBuffers && _currentRoom != 0 && isLightOn()) {
			if (vs->number != kMainVirtScreen) {
				// Restore from back buffer
				const byte *backBuf = vs->getBackPixels(0, 0);
				blit(screenBuf, vs->pitch, backBuf, vs->pitch, vs->w, vs->h);
			}
		} else {
			// Clear area
			memset(screenBuf, 0, vs->h * vs->pitch);
		}

		if (vs->hasTwoBuffers) {
			// Clean out the charset mask
			clearTextSurface();
		}
	}
}

void ScummEngine::clearCharsetMask() {
	memset(getResourceAddress(rtBuffer, 9), 0, _gdi->_imgBufOffs[1]);
}

void ScummEngine::clearTextSurface() {
	memset(_textSurface.pixels, CHARSET_MASK_TRANSPARENCY, _textSurface.pitch * _textSurface.h);
}

byte *ScummEngine::getMaskBuffer(int x, int y, int z) {
	return _gdi->getMaskBuffer((x + _virtscr[kMainVirtScreen].xstart) / 8, y, z);
}

byte *Gdi::getMaskBuffer(int x, int y, int z) {
	return _vm->getResourceAddress(rtBuffer, 9)
			+ x + y * _numStrips + _imgBufOffs[z];
}


#pragma mark -
#pragma mark --- Misc ---
#pragma mark -

static void blit(byte *dst, int dstPitch, const byte *src, int srcPitch, int w, int h) {
	assert(w > 0);
	assert(h > 0);
	assert(src != NULL);
	assert(dst != NULL);

	if (w == srcPitch && w == dstPitch) {
		memcpy(dst, src, w*h);
	} else {
		do {
			memcpy(dst, src, w);
			dst += dstPitch;
			src += srcPitch;
		} while (--h);
	}
}

static void fill(byte *dst, int dstPitch, byte color, int w, int h) {
	assert(h > 0);
	assert(dst != NULL);

	if (w == dstPitch) {
		memset(dst, color, w*h);
	} else {
		do {
			memset(dst, color, w);
			dst += dstPitch;
		} while (--h);
	}
}

#ifdef ARM_USE_GFX_ASM

#define copy8Col(A,B,C,D) asmCopy8Col(A,B,C,D)

#else

static void copy8Col(byte *dst, int dstPitch, const byte *src, int height) {

	do {
#if defined(SCUMM_NEED_ALIGNMENT)
		memcpy(dst, src, 8);
#else
		((uint32 *)dst)[0] = ((const uint32 *)src)[0];
		((uint32 *)dst)[1] = ((const uint32 *)src)[1];
#endif
		dst += dstPitch;
		src += dstPitch;
	} while (--height);
}

#endif /* ARM_USE_GFX_ASM */

static void clear8Col(byte *dst, int dstPitch, int height) {
	do {
#if defined(SCUMM_NEED_ALIGNMENT)
		memset(dst, 0, 8);
#else
		((uint32 *)dst)[0] = 0;
		((uint32 *)dst)[1] = 0;
#endif
		dst += dstPitch;
	} while (--height);
}

void ScummEngine::drawBox(int x, int y, int x2, int y2, int color) {
	int width, height;
	VirtScreen *vs;
	byte *backbuff, *bgbuff;

	if ((vs = findVirtScreen(y)) == NULL)
		return;

	if (x > x2)
		SWAP(x, x2);

	if (y > y2)
		SWAP(y, y2);

	x2++;
	y2++;

	// Adjust for the topline of the VirtScreen
	y -= vs->topline;
	y2 -= vs->topline;

	// Clip the coordinates
	if (x < 0)
		x = 0;
	else if (x >= vs->w)
		return;

	if (x2 < 0)
		return;
	else if (x2 > vs->w)
		x2 = vs->w;

	if (y < 0)
		y = 0;
	else if (y > vs->h)
		return;

	if (y2 < 0)
		return;
	else if (y2 > vs->h)
		y2 = vs->h;

	width = x2 - x;
	height = y2 - y;

	// This will happen in the Sam & Max intro - see bug #1039162 - where
	// it would trigger an assertion in blit().

	if (width <= 0 || height <= 0)
		return;

	markRectAsDirty(vs->number, x, x2, y, y2);

	backbuff = vs->getPixels(x, y);
	bgbuff = vs->getBackPixels(x, y);

	if (color == -1) {
		if (vs->number != kMainVirtScreen)
			error("can only copy bg to main window");
		blit(backbuff, vs->pitch, bgbuff, vs->pitch, width, height);
		if (_charset->_hasMask) {
			byte *mask = (byte *)_textSurface.getBasePtr(x * _textSurfaceMultiplier, (y - _screenTop) * _textSurfaceMultiplier);
			fill(mask, _textSurface.pitch, CHARSET_MASK_TRANSPARENCY, width * _textSurfaceMultiplier, height * _textSurfaceMultiplier);
		}
	} else if (_game.heversion >= 72) {
		// Flags are used for different methods in HE games
		uint32 flags = color;
		if ((flags & 0x2000) || (flags & 0x4000000)) {
			blit(backbuff, vs->pitch, bgbuff, vs->pitch, width, height);
		} else if ((flags & 0x4000) || (flags & 0x2000000)) {
			blit(bgbuff, vs->pitch, backbuff, vs->pitch, width, height);
		} else if ((flags & 0x8000) || (flags & 0x1000000)) {
			flags &= (flags & 0x1000000) ? 0xFFFFFF : 0x7FFF;
			fill(backbuff, vs->pitch, flags, width, height);
			fill(bgbuff, vs->pitch, flags, width, height);
		} else {
			fill(backbuff, vs->pitch, flags, width, height);
		}
	} else if (_game.heversion >= 60) {
		// Flags are used for different methods in HE games
		uint16 flags = color;
		if (flags & 0x2000) {
			blit(backbuff, vs->pitch, bgbuff, vs->pitch, width, height);
		} else if (flags & 0x4000) {
			blit(bgbuff, vs->pitch, backbuff, vs->pitch, width, height);
		} else if (flags & 0x8000) {
			flags &= 0x7FFF;
			fill(backbuff, vs->pitch, flags, width, height);
			fill(bgbuff, vs->pitch, flags, width, height);
		} else {
			fill(backbuff, vs->pitch, flags, width, height);
		}
	} else {
		fill(backbuff, vs->pitch, color, width, height);
	}
}

/**
 * Moves the screen content by the offset specified via dx/dy.
 * Only the region from x=0 till x=height-1 is affected.
 * @param dx	the horizontal offset.
 * @param dy	the vertical offset.
 * @param height	the number of lines which in which the move will be done.
 */
void ScummEngine::moveScreen(int dx, int dy, int height) {
	// Short circuit check - do we have to do anything anyway?
	if ((dx == 0 && dy == 0) || height <= 0)
		return;

	Graphics::Surface *screen = _system->lockScreen();
	if (!screen)
		return;
	screen->move(dx, dy, height);
	_system->unlockScreen();
}

void ScummEngine_v5::clearFlashlight() {
	_flashlight.isDrawn = false;
	_flashlight.buffer = NULL;
}

void ScummEngine_v5::drawFlashlight() {
	int i, j, x, y;
	VirtScreen *vs = &_virtscr[kMainVirtScreen];

	// Remove the flash light first if it was previously drawn
	if (_flashlight.isDrawn) {
		markRectAsDirty(kMainVirtScreen, _flashlight.x, _flashlight.x + _flashlight.w,
										_flashlight.y, _flashlight.y + _flashlight.h, USAGE_BIT_DIRTY);

		if (_flashlight.buffer) {
			fill(_flashlight.buffer, vs->pitch, 0, _flashlight.w, _flashlight.h);
		}
		_flashlight.isDrawn = false;
	}

	if (_flashlight.xStrips == 0 || _flashlight.yStrips == 0)
		return;

	// Calculate the area of the flashlight
	if (_game.id == GID_ZAK || _game.id == GID_MANIAC) {
		x = _mouse.x + vs->xstart;
		y = _mouse.y - vs->topline;
	} else {
		Actor *a = derefActor(VAR(VAR_EGO), "drawFlashlight");
		x = a->getPos().x;
		y = a->getPos().y;
	}
	_flashlight.w = _flashlight.xStrips * 8;
	_flashlight.h = _flashlight.yStrips * 8;
	_flashlight.x = x - _flashlight.w / 2 - _screenStartStrip * 8;
	_flashlight.y = y - _flashlight.h / 2;

	if (_game.id == GID_LOOM)
		_flashlight.y -= 12;

	// Clip the flashlight at the borders
	if (_flashlight.x < 0)
		_flashlight.x = 0;
	else if (_flashlight.x + _flashlight.w > _gdi->_numStrips * 8)
		_flashlight.x = _gdi->_numStrips * 8 - _flashlight.w;
	if (_flashlight.y < 0)
		_flashlight.y = 0;
	else if (_flashlight.y + _flashlight.h> vs->h)
		_flashlight.y = vs->h - _flashlight.h;

	// Redraw any actors "under" the flashlight
	for (i = _flashlight.x / 8; i < (_flashlight.x + _flashlight.w) / 8; i++) {
		assert(0 <= i && i < _gdi->_numStrips);
		setGfxUsageBit(_screenStartStrip + i, USAGE_BIT_DIRTY);
		vs->tdirty[i] = 0;
		vs->bdirty[i] = vs->h;
	}

	byte *bgbak;
	_flashlight.buffer = vs->getPixels(_flashlight.x, _flashlight.y);
	bgbak = vs->getBackPixels(_flashlight.x, _flashlight.y);

	blit(_flashlight.buffer, vs->pitch, bgbak, vs->pitch, _flashlight.w, _flashlight.h);

	// Round the corners. To do so, we simply hard-code a set of nicely
	// rounded corners.
	static const int corner_data[] = { 8, 6, 4, 3, 2, 2, 1, 1 };
	int minrow = 0;
	int maxcol = _flashlight.w - 1;
	int maxrow = (_flashlight.h - 1) * vs->pitch;

	for (i = 0; i < 8; i++, minrow += vs->pitch, maxrow -= vs->pitch) {
		int d = corner_data[i];

		for (j = 0; j < d; j++) {
			_flashlight.buffer[minrow + j] = 0;
			_flashlight.buffer[minrow + maxcol - j] = 0;
			_flashlight.buffer[maxrow + j] = 0;
			_flashlight.buffer[maxrow + maxcol - j] = 0;
		}
	}

	_flashlight.isDrawn = true;
}

// V0 Maniac doesn't have a ScummVar for VAR_CURRENT_LIGHTS, and just uses
// an internal variable. Emulate this to prevent overwriting script vars...
// And V6 games do not use the "lights" at all. There, the whole screen is
// always visible, and actors are always colored, so we fake the correct
// light value for it.
int ScummEngine::getCurrentLights() const {
	if (_game.id == GID_MANIAC && _game.version == 0)
		return _currentLights;
	else if (_game.version >= 6)
		return LIGHTMODE_room_lights_on | LIGHTMODE_actor_use_colors;
	else
		return VAR(VAR_CURRENT_LIGHTS);
}

bool ScummEngine::isLightOn() const {
	return (getCurrentLights() & LIGHTMODE_room_lights_on) != 0;
}

void ScummEngine::setShake(int mode) {
	if (_shakeEnabled != (mode != 0))
		_fullRedraw = true;

	_shakeEnabled = mode != 0;
	_shakeFrame = 0;
	_system->setShakePos(0);
}

#pragma mark -
#pragma mark --- Image drawing ---
#pragma mark -


void Gdi::prepareDrawBitmap(const byte *ptr, VirtScreen *vs,
					const int x, const int y, const int width, const int height,
	                int stripnr, int numstrip) {
	// Do nothing by default
}

void GdiV1::prepareDrawBitmap(const byte *ptr, VirtScreen *vs,
					const int x, const int y, const int width, const int height,
	                int stripnr, int numstrip) {
	if (_objectMode) {
		decodeC64Gfx(ptr, _C64.objectMap, (width / 8) * (height / 8) * 3);
	}
}

void GdiNES::prepareDrawBitmap(const byte *ptr, VirtScreen *vs,
					const int x, const int y, const int width, const int height,
	                int stripnr, int numstrip) {
	if (_objectMode) {
		decodeNESObject(ptr, x - stripnr, y, width, height);
	}
}


void GdiV2::prepareDrawBitmap(const byte *ptr, VirtScreen *vs,
					const int x, const int y, const int width, const int height,
	                int stripnr, int numstrip) {
	//
	// Since V3, all graphics data was encoded in strips, which is very efficient
	// for redrawing only parts of the screen. However, V2 is different: here
	// the whole graphics are encoded as one big chunk. That makes it rather
	// dificult to draw only parts of a room/object. We handle the V2 graphics
	// differently from all other (newer) graphic formats for this reason.
	//
	StripTable *table = (_objectMode ? 0 : _roomStrips);
	const int left = (stripnr * 8);
	const int right = left + (numstrip * 8);
	byte *dst;
	byte *mask_ptr;
	const byte *src;
	byte color, data = 0;
	int run;
	bool dither = false;
	byte dither_table[128];
	byte *ptr_dither_table;
	int theX, theY, maxX;

	memset(dither_table, 0, sizeof(dither_table));

	if (vs->hasTwoBuffers)
		dst = vs->backBuf + y * vs->pitch + x * 8;
	else
		dst = (byte *)vs->pixels + y * vs->pitch + x * 8;

	mask_ptr = getMaskBuffer(x, y, 1);


	if (table) {
		run = table->run[stripnr];
		color = table->color[stripnr];
		src = ptr + table->offsets[stripnr];
		theX = left;
		maxX = right;
	} else {
		run = 1;
		color = 0;
		src = ptr;
		theX = 0;
		maxX = width;
	}

	// Decode and draw the image data.
	assert(height <= 128);
	for (; theX < maxX; theX++) {
		ptr_dither_table = dither_table;
		for (theY = 0; theY < height; theY++) {
			if (--run == 0) {
				data = *src++;
				if (data & 0x80) {
					run = data & 0x7f;
					dither = true;
				} else {
					run = data >> 4;
					dither = false;
				}
				color = _roomPalette[data & 0x0f];
				if (run == 0) {
					run = *src++;
				}
			}
			if (!dither) {
				*ptr_dither_table = color;
			}
			if (left <= theX && theX < right) {
				*dst = *ptr_dither_table++;
				dst += vs->pitch;
			}
		}
		if (left <= theX && theX < right) {
			dst -= _vertStripNextInc;
		}
	}


	// Draw mask (zplane) data
	theY = 0;

	if (table) {
		src = ptr + table->zoffsets[stripnr];
		run = table->zrun[stripnr];
		theX = left;
	} else {
		run = *src++;
		theX = 0;
	}
	while (theX < right) {
		const byte runFlag = run & 0x80;
		if (runFlag) {
			run &= 0x7f;
			data = *src++;
		}
		do {
			if (!runFlag)
				data = *src++;

			if (left <= theX) {
				*mask_ptr = data;
				mask_ptr += _numStrips;
			}
			theY++;
			if (theY >= height) {
				if (left <= theX) {
					mask_ptr -= _numStrips * height - 1;
				}
				theY = 0;
				theX += 8;
				if (theX >= right)
					break;
			}
		} while (--run);
		run = *src++;
	}
}

int Gdi::getZPlanes(const byte *ptr, const byte *zplane_list[9], bool bmapImage) const {
	int numzbuf;
	int i;

	if ((_vm->_game.features & GF_SMALL_HEADER) || _vm->_game.version == 8)
		zplane_list[0] = ptr;
	else if (bmapImage)
		zplane_list[0] = _vm->findResource(MKID_BE('BMAP'), ptr);
	else
		zplane_list[0] = _vm->findResource(MKID_BE('SMAP'), ptr);

	if (_zbufferDisabled)
		numzbuf = 0;
	else if (_numZBuffer <= 1 || (_vm->_game.version <= 2))
		numzbuf = _numZBuffer;
	else {
		numzbuf = _numZBuffer;
		assert(numzbuf <= 9);

		if (_vm->_game.features & GF_SMALL_HEADER) {
			if (_vm->_game.features & GF_16COLOR)
				zplane_list[1] = ptr + READ_LE_UINT16(ptr);
			else {
				zplane_list[1] = ptr + READ_LE_UINT32(ptr);
				if (_vm->_game.features & GF_OLD256) {
					if (0 == READ_LE_UINT32(zplane_list[1]))
						zplane_list[1] = 0;
				}
			}
			for (i = 2; i < numzbuf; i++) {
				zplane_list[i] = zplane_list[i-1] + READ_LE_UINT16(zplane_list[i-1]);
			}
		} else if (_vm->_game.version == 8) {
			// Find the OFFS chunk of the ZPLN chunk
			const byte *zplnOffsChunkStart = ptr + 24 + READ_BE_UINT32(ptr + 12);

			// Each ZPLN contains a WRAP chunk, which has (as always) an OFFS subchunk pointing
			// at ZSTR chunks. These once more contain a WRAP chunk which contains nothing but
			// an OFFS chunk. The content of this OFFS chunk contains the offsets to the
			// Z-planes.
			// We do not directly make use of this, but rather hard code offsets (like we do
			// for all other Scumm-versions, too). Clearly this is a bit hackish, but works
			// well enough, and there is no reason to assume that there are any cases where it
			// might fail. Still, doing this properly would have the advantage of catching
			// invalid/damaged data files, and allow us to exit gracefully instead of segfaulting.
			for (i = 1; i < numzbuf; i++) {
				zplane_list[i] = zplnOffsChunkStart + READ_LE_UINT32(zplnOffsChunkStart + 4 + i*4) + 16;
			}
		} else {
			const uint32 zplane_tags[] = {
				MKID_BE('ZP00'),
				MKID_BE('ZP01'),
				MKID_BE('ZP02'),
				MKID_BE('ZP03'),
				MKID_BE('ZP04')
			};

			for (i = 1; i < numzbuf; i++) {
				zplane_list[i] = _vm->findResource(zplane_tags[i], ptr);
			}
		}
	}

	return numzbuf;
}

/**
 * Draw a bitmap onto a virtual screen. This is main drawing method for room backgrounds
 * and objects, used throughout all SCUMM versions.
 */
void Gdi::drawBitmap(const byte *ptr, VirtScreen *vs, int x, const int y, const int width, const int height,
					int stripnr, int numstrip, byte flag) {
	assert(ptr);
	assert(height > 0);

	byte *dstPtr;
	const byte *smap_ptr;
	const byte *zplane_list[9];
	int numzbuf;
	int sx;
	bool transpStrip = false;

	// Check whether lights are turned on or not
	const bool lightsOn = _vm->isLightOn();

	if (_vm->_game.id == GID_LOOM && _vm->_game.platform == Common::kPlatformPCEngine) {
		// FIXME: Image format unknown
		return;
	}

	if (_vm->_game.features & GF_SMALL_HEADER) {
		smap_ptr = ptr;
	} else if (_vm->_game.version == 8) {
		// Skip to the BSTR->WRAP->OFFS chunk
		smap_ptr = ptr + 24;
	} else {
		smap_ptr = _vm->findResource(MKID_BE('SMAP'), ptr);
		assert(smap_ptr);
	}

	numzbuf = getZPlanes(ptr, zplane_list, false);

	const byte *tmsk_ptr = NULL;
	if (_vm->_game.heversion >= 72) {
		tmsk_ptr = _vm->findResource(MKID_BE('TMSK'), ptr);
	}

	if (y + height > vs->h) {
		warning("Gdi::drawBitmap, strip drawn to %d below window bottom %d", y + height, vs->h);
	}

	_vertStripNextInc = height * vs->pitch - 1;

	_objectMode = (flag & dbObjectMode) == dbObjectMode;
	prepareDrawBitmap(ptr, vs, x, y, width, height, stripnr, numstrip);

	sx = x - vs->xstart / 8;
	if (sx < 0) {
		numstrip -= -sx;
		x += -sx;
		stripnr += -sx;
		sx = 0;
	}

	// Compute the number of strips we have to iterate over.
	// TODO/FIXME: The computation of its initial value looks very fishy.
	// It was added as a kind of hack to fix some corner cases, but it compares
	// the room width to the virtual screen width; but the former should always
	// be bigger than the latter (except for MM NES, maybe)... strange
	int limit = MAX(_vm->_roomWidth, (int) vs->w) / 8 - x;
	if (limit > numstrip)
		limit = numstrip;
	if (limit > _numStrips - sx)
		limit = _numStrips - sx;
	for (int k = 0; k < limit; ++k, ++stripnr, ++sx, ++x) {
		if (y < vs->tdirty[sx])
			vs->tdirty[sx] = y;

		if (y + height > vs->bdirty[sx])
			vs->bdirty[sx] = y + height;

		// In the case of a double buffered virtual screen, we draw to
		// the backbuffer, otherwise to the primary surface memory.
		if (vs->hasTwoBuffers)
			dstPtr = vs->backBuf + y * vs->pitch + x * 8;
		else
			dstPtr = (byte *)vs->pixels + y * vs->pitch + x * 8;

		transpStrip = drawStrip(dstPtr, vs, x, y, width, height, stripnr, smap_ptr);

		// COMI and HE games only uses flag value
		if (_vm->_game.version == 8 || _vm->_game.heversion >= 60)
			transpStrip = true;

		if (vs->hasTwoBuffers) {
			byte *frontBuf = (byte *)vs->pixels + y * vs->pitch + x * 8;
			if (lightsOn)
				copy8Col(frontBuf, vs->pitch, dstPtr, height);
			else
				clear8Col(frontBuf, vs->pitch, height);
		}

		decodeMask(x, y, width, height, stripnr, numzbuf, zplane_list, transpStrip, flag, tmsk_ptr);

#if 0
		// HACK: blit mask(s) onto normal screen. Useful to debug masking
		for (int i = 0; i < numzbuf; i++) {
			byte *dst1, *dst2;

			dst1 = dst2 = (byte *)vs->pixels + y * vs->pitch + x * 8;
			if (vs->hasTwoBuffers)
				dst2 = vs->backBuf + y * vs->pitch + x * 8;
			byte *mask_ptr = getMaskBuffer(x, y, i);

			for (int h = 0; h < height; h++) {
				int maskbits = *mask_ptr;
				for (int j = 0; j < 8; j++) {
					if (maskbits & 0x80)
						dst1[j] = dst2[j] = 12 + i;
					maskbits <<= 1;
				}
				dst1 += vs->pitch;
				dst2 += vs->pitch;
				mask_ptr += _numStrips;
			}
		}
#endif
	}
}

bool Gdi::drawStrip(byte *dstPtr, VirtScreen *vs, int x, int y, const int width, const int height,
					int stripnr, const byte *smap_ptr) {
	// Do some input verification and make sure the strip/strip offset
	// are actually valid. Normally, this should never be a problem,
	// but if e.g. a savegame gets corrupted, we can easily get into
	// trouble here. See also bug #795214.
	int offset = -1, smapLen;
	if (_vm->_game.id == GID_LOOM && _vm->_game.platform == Common::kPlatformPCEngine) {
		// Length of offsets segment only
		smapLen = READ_LE_UINT16(smap_ptr);
		if (stripnr * 2 + 2 < smapLen) {
			offset = READ_LE_UINT16(smap_ptr + stripnr * 2 + 2);
			offset += stripnr * 2 + 3;
		}
		debug(0, "stripnr %d len %d offset %d", stripnr, smapLen, offset);
	} else if (_vm->_game.features & GF_16COLOR) {
		smapLen = READ_LE_UINT16(smap_ptr);
		if (stripnr * 2 + 2 < smapLen) {
			offset = READ_LE_UINT16(smap_ptr + stripnr * 2 + 2);
		}
		assertRange(0, offset, smapLen-1, "screen strip");
	} else if (_vm->_game.features & GF_SMALL_HEADER) {
		smapLen = READ_LE_UINT32(smap_ptr);
		if (stripnr * 4 + 4 < smapLen)
			offset = READ_LE_UINT32(smap_ptr + stripnr * 4 + 4);
		assertRange(0, offset, smapLen-1, "screen strip");
	} else {
		smapLen = READ_BE_UINT32(smap_ptr);
		if (stripnr * 4 + 8 < smapLen)
			offset = READ_LE_UINT32(smap_ptr + stripnr * 4 + 8);
		assertRange(0, offset, smapLen-1, "screen strip");
	}

	return decompressBitmap(dstPtr, vs->pitch, smap_ptr + offset, height);
}

bool GdiNES::drawStrip(byte *dstPtr, VirtScreen *vs, int x, int y, const int width, const int height,
					int stripnr, const byte *smap_ptr) {
	byte *mask_ptr = getMaskBuffer(x, y, 1);
	drawStripNES(dstPtr, mask_ptr, vs->pitch, stripnr, y, height);

	return false;
}

bool GdiV1::drawStrip(byte *dstPtr, VirtScreen *vs, int x, int y, const int width, const int height,
					int stripnr, const byte *smap_ptr) {
	if (_objectMode)
		drawStripC64Object(dstPtr, vs->pitch, stripnr, width, height);
	else
		drawStripC64Background(dstPtr, vs->pitch, stripnr, height);

	return false;
}

bool GdiV2::drawStrip(byte *dstPtr, VirtScreen *vs, int x, int y, const int width, const int height,
					int stripnr, const byte *smap_ptr) {
	// Do nothing here for V2 games - drawing was already handled.
	return false;
}

void Gdi::decodeMask(int x, int y, const int width, const int height,
	                int stripnr, int numzbuf, const byte *zplane_list[9],
	                bool transpStrip, byte flag, const byte *tmsk_ptr) {
	int i;
	byte *mask_ptr;
	const byte *z_plane_ptr;

	if (flag & dbDrawMaskOnAll) {
		// Sam & Max uses dbDrawMaskOnAll for things like the inventory
		// box and the speech icons. While these objects only have one
		// mask, it should be applied to all the Z-planes in the room,
		// i.e. they should mask every actor.
		//
		// This flag used to be called dbDrawMaskOnBoth, and all it
		// would do was to mask Z-plane 0. (Z-plane 1 would also be
		// masked, because what is now the else-clause used to be run
		// always.) While this seems to be the only way there is to
		// mask Z-plane 0, this wasn't good enough since actors in
		// Z-planes >= 2 would not be masked.
		//
		// The flag is also used by The Dig and Full Throttle, but I
		// don't know what for. At the time of writing, these games
		// are still too unstable for me to investigate.

		if (_vm->_game.version == 8)
			z_plane_ptr = zplane_list[1] + READ_LE_UINT32(zplane_list[1] + stripnr * 4 + 8);
		else
			z_plane_ptr = zplane_list[1] + READ_LE_UINT16(zplane_list[1] + stripnr * 2 + 8);
		for (i = 0; i < numzbuf; i++) {
			mask_ptr = getMaskBuffer(x, y, i);
			if (transpStrip && (flag & dbAllowMaskOr))
				decompressMaskImgOr(mask_ptr, z_plane_ptr, height);
			else
				decompressMaskImg(mask_ptr, z_plane_ptr, height);
		}
	} else {
		for (i = 1; i < numzbuf; i++) {
			uint32 offs;

			if (!zplane_list[i])
				continue;

			if (_vm->_game.features & GF_OLD_BUNDLE)
				offs = READ_LE_UINT16(zplane_list[i] + stripnr * 2);
			else if (_vm->_game.features & GF_OLD256)
				offs = READ_LE_UINT16(zplane_list[i] + stripnr * 2 + 4);
			else if (_vm->_game.features & GF_SMALL_HEADER)
				offs = READ_LE_UINT16(zplane_list[i] + stripnr * 2 + 2);
			else if (_vm->_game.version == 8)
				offs = READ_LE_UINT32(zplane_list[i] + stripnr * 4 + 8);
			else
				offs = READ_LE_UINT16(zplane_list[i] + stripnr * 2 + 8);

			mask_ptr = getMaskBuffer(x, y, i);

			if (offs) {
				z_plane_ptr = zplane_list[i] + offs;

				if (tmsk_ptr) {
					const byte *tmsk = tmsk_ptr + READ_LE_UINT16(tmsk_ptr + 8);
					decompressTMSK(mask_ptr, tmsk, z_plane_ptr, height);
				} else if (transpStrip && (flag & dbAllowMaskOr)) {
					decompressMaskImgOr(mask_ptr, z_plane_ptr, height);
				} else {
					decompressMaskImg(mask_ptr, z_plane_ptr, height);
				}

			} else {
				if (!(transpStrip && (flag & dbAllowMaskOr)))
					for (int h = 0; h < height; h++)
						mask_ptr[h * _numStrips] = 0;
			}
		}
	}
}

void GdiNES::decodeMask(int x, int y, const int width, const int height,
	                int stripnr, int numzbuf, const byte *zplane_list[9],
	                bool transpStrip, byte flag, const byte *tmsk_ptr) {
	byte *mask_ptr = getMaskBuffer(x, y, 1);
	drawStripNESMask(mask_ptr, stripnr, y, height);
}

void GdiV1::decodeMask(int x, int y, const int width, const int height,
	                int stripnr, int numzbuf, const byte *zplane_list[9],
	                bool transpStrip, byte flag, const byte *tmsk_ptr) {
	byte *mask_ptr = getMaskBuffer(x, y, 1);
	drawStripC64Mask(mask_ptr, stripnr, width, height);
}

void GdiV2::decodeMask(int x, int y, const int width, const int height,
	                int stripnr, int numzbuf, const byte *zplane_list[9],
	                bool transpStrip, byte flag, const byte *tmsk_ptr) {
	// Do nothing here for V2 games - zplane was already handled.
}

#ifdef ENABLE_HE
/**
 * Draw a bitmap onto a virtual screen. This is main drawing method for room backgrounds
 * used throughout HE71+ versions.
 *
 * @note This function essentially is a stripped down & special cased version of
 * the generic Gdi::drawBitmap() method.
 */
void Gdi::drawBMAPBg(const byte *ptr, VirtScreen *vs) {
	const byte *z_plane_ptr;
	byte *mask_ptr;
	const byte *zplane_list[9];

	const byte *bmap_ptr = _vm->findResourceData(MKID_BE('BMAP'), ptr);
	assert(bmap_ptr);

	byte code = *bmap_ptr++;
	byte *dst = vs->getBackPixels(0, 0);

	// The following few lines more or less duplicate decompressBitmap(), only
	// for an area spanning multiple strips. In particular, the codecs 13 & 14
	// in decompressBitmap call drawStripHE()
	_decomp_shr = code % 10;
	_decomp_mask = 0xFF >> (8 - _decomp_shr);

	switch (code) {
	case 134:
	case 135:
	case 136:
	case 137:
	case 138:
		drawStripHE(dst, vs->pitch, bmap_ptr, vs->w, vs->h, false);
		break;
	case 144:
	case 145:
	case 146:
	case 147:
	case 148:
		drawStripHE(dst, vs->pitch, bmap_ptr, vs->w, vs->h, true);
		break;
	case 150:
		fill(dst, vs->pitch, *bmap_ptr, vs->w, vs->h);
		break;
	default:
		// Alternative russian freddi3 uses badly formatted bitmaps
		debug(0, "Gdi::drawBMAPBg: default case %d", code);
	}

	((ScummEngine_v71he *)_vm)->restoreBackgroundHE(Common::Rect(vs->w, vs->h));

	int numzbuf = getZPlanes(ptr, zplane_list, true);
	if (numzbuf <= 1)
		return;

	uint32 offs;
	for (int stripnr = 0; stripnr < _numStrips; stripnr++) {
		for (int i = 1; i < numzbuf; i++) {
			if (!zplane_list[i])
				continue;

			offs = READ_LE_UINT16(zplane_list[i] + stripnr * 2 + 8);
			mask_ptr = getMaskBuffer(stripnr, 0, i);

			if (offs) {
				z_plane_ptr = zplane_list[i] + offs;
				decompressMaskImg(mask_ptr, z_plane_ptr, vs->h);
			}
		}

#if 0
		// HACK: blit mask(s) onto normal screen. Useful to debug masking
		for (int i = 0; i < numzbuf; i++) {
			byte *dst1 = (byte *)vs->pixels + stripnr * 8;
			byte *dst2 = vs->backBuf + stripnr * 8;

			mask_ptr = getMaskBuffer(stripnr, 0, i);
			for (int h = 0; h < vs->h; h++) {
				int maskbits = *mask_ptr;
				for (int j = 0; j < 8; j++) {
					if (maskbits & 0x80)
						dst1[j] = dst2[j] = 12 + i;
					maskbits <<= 1;
				}
				dst1 += vs->pitch;
				dst2 += vs->pitch;
				mask_ptr += _numStrips;
			}
		}
#endif
	}
}

void Gdi::drawBMAPObject(const byte *ptr, VirtScreen *vs, int obj, int x, int y, int w, int h) {
	const byte *bmap_ptr = _vm->findResourceData(MKID_BE('BMAP'), ptr);
	assert(bmap_ptr);

	byte code = *bmap_ptr++;
	int scrX = _vm->_screenStartStrip * 8;

	if (code == 8 || code == 9) {
		Common::Rect rScreen(0, 0, vs->w, vs->h);
		byte *dst = (byte *)_vm->_virtscr[kMainVirtScreen].backBuf + scrX;
		Wiz::copyWizImage(dst, bmap_ptr, vs->w, vs->h, x - scrX, y, w, h, &rScreen);
	}

	Common::Rect rect1(x, y, x + w, y + h);
	Common::Rect rect2(scrX, 0, vs->w + scrX, vs->h);

	if (rect1.intersects(rect2)) {
		rect1.clip(rect2);
		rect1.left -= rect2.left;
		rect1.right -= rect2.left;
		rect1.top -= rect2.top;
		rect1.bottom -= rect2.top;

		((ScummEngine_v71he *)_vm)->restoreBackgroundHE(rect1);
	}
}

void ScummEngine_v70he::restoreBackgroundHE(Common::Rect rect, int dirtybit) {
	byte *src, *dst;
	VirtScreen *vs = &_virtscr[kMainVirtScreen];

	if (rect.top > vs->h || rect.bottom < 0)
		return;

	if (rect.left > vs->w || rect.right < 0)
		return;

	rect.left = MAX(0, (int)rect.left);
	rect.left = MIN((int)rect.left, (int)vs->w - 1);

	rect.right = MAX(0, (int)rect.right);
	rect.right = MIN((int)rect.right, (int)vs->w);

	rect.top = MAX(0, (int)rect.top);
	rect.top = MIN((int)rect.top, (int)vs->h - 1);

	rect.bottom = MAX(0, (int)rect.bottom);
	rect.bottom = MIN((int)rect.bottom, (int)vs->h);

	const int rw = rect.width();
	const int rh = rect.height();

	if (rw == 0 || rh == 0)
		return;

	src = _virtscr[kMainVirtScreen].getBackPixels(rect.left, rect.top);
	dst = _virtscr[kMainVirtScreen].getPixels(rect.left, rect.top);

	assert(rw <= _screenWidth && rw > 0);
	assert(rh <= _screenHeight && rh > 0);
	blit(dst, _virtscr[kMainVirtScreen].pitch, src, _virtscr[kMainVirtScreen].pitch, rw, rh);
	markRectAsDirty(kMainVirtScreen, rect, dirtybit);
}
#endif

/**
 * Reset the background behind an actor or blast object.
 */
void Gdi::resetBackground(int top, int bottom, int strip) {
	VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
	byte *backbuff_ptr, *bgbak_ptr;
	int numLinesToProcess;

	if (top < 0)
		top = 0;

	if (bottom > vs->h)
		bottom = vs->h;

	if (top >= bottom)
		return;

	assert(0 <= strip && strip < _numStrips);

	if (top < vs->tdirty[strip])
		vs->tdirty[strip] = top;

	if (bottom > vs->bdirty[strip])
		vs->bdirty[strip] = bottom;

	bgbak_ptr = (byte *)vs->backBuf + top * vs->pitch + (strip + vs->xstart/8) * 8;
	backbuff_ptr = (byte *)vs->pixels + top * vs->pitch + (strip + vs->xstart/8) * 8;

	numLinesToProcess = bottom - top;
	if (numLinesToProcess) {
		if (_vm->isLightOn()) {
			copy8Col(backbuff_ptr, vs->pitch, bgbak_ptr, numLinesToProcess);
		} else {
			clear8Col(backbuff_ptr, vs->pitch, numLinesToProcess);
		}
	}
}

bool Gdi::decompressBitmap(byte *dst, int dstPitch, const byte *src, int numLinesToProcess) {
	assert(numLinesToProcess);

	if (_vm->_game.features & GF_16COLOR) {
		drawStripEGA(dst, dstPitch, src, numLinesToProcess);
		return false;
	}

	if ((_vm->_game.platform == Common::kPlatformAmiga) && (_vm->_game.version >= 4))
		_paletteMod = 16;
	else
		_paletteMod = 0;

	byte code = *src++;
	bool transpStrip = false;
	_decomp_shr = code % 10;
	_decomp_mask = 0xFF >> (8 - _decomp_shr);

	switch (code) {
	case 1:
		drawStripRaw(dst, dstPitch, src, numLinesToProcess, false);
		break;

	case 2:
		unkDecode8(dst, dstPitch, src, numLinesToProcess);       /* Ender - Zak256/Indy256 */
		break;

	case 3:
		unkDecode9(dst, dstPitch, src, numLinesToProcess);       /* Ender - Zak256/Indy256 */
		break;

	case 4:
		unkDecode10(dst, dstPitch, src, numLinesToProcess);      /* Ender - Zak256/Indy256 */
		break;

	case 7:
		unkDecode11(dst, dstPitch, src, numLinesToProcess);      /* Ender - Zak256/Indy256 */
		break;

	case 8:
		// Used in 3DO versions of HE games
		transpStrip = true;
		drawStrip3DO(dst, dstPitch, src, numLinesToProcess, true);
		break;

	case 9:
		drawStrip3DO(dst, dstPitch, src, numLinesToProcess, false);
		break;

	case 10:
		// Used in Amiga version of Monkey Island 1
		drawStripEGA(dst, dstPitch, src, numLinesToProcess);
		break;

	case 14:
	case 15:
	case 16:
	case 17:
	case 18:
		drawStripBasicV(dst, dstPitch, src, numLinesToProcess, false);
		break;

	case 24:
	case 25:
	case 26:
	case 27:
	case 28:
		drawStripBasicH(dst, dstPitch, src, numLinesToProcess, false);
		break;

	case 34:
	case 35:
	case 36:
	case 37:
	case 38:
		transpStrip = true;
		drawStripBasicV(dst, dstPitch, src, numLinesToProcess, true);
		break;

	case 44:
	case 45:
	case 46:
	case 47:
	case 48:
		transpStrip = true;
		drawStripBasicH(dst, dstPitch, src, numLinesToProcess, true);
		break;

	case 64:
	case 65:
	case 66:
	case 67:
	case 68:
	case 104:
	case 105:
	case 106:
	case 107:
	case 108:
		drawStripComplex(dst, dstPitch, src, numLinesToProcess, false);
		break;

	case 84:
	case 85:
	case 86:
	case 87:
	case 88:
	case 124:
	case 125:
	case 126:
	case 127:
	case 128:
		transpStrip = true;
		drawStripComplex(dst, dstPitch, src, numLinesToProcess, true);
		break;

	case 134:
	case 135:
	case 136:
	case 137:
	case 138:
		drawStripHE(dst, dstPitch, src, 8, numLinesToProcess, false);
		break;

	case 143: // Triggered by Russian water
	case 144:
	case 145:
	case 146:
	case 147:
	case 148:
		transpStrip = true;
		drawStripHE(dst, dstPitch, src, 8, numLinesToProcess, true);
		break;

	case 149:
		drawStripRaw(dst, dstPitch, src, numLinesToProcess, true);
		break;

	default:
		error("Gdi::decompressBitmap: default case %d", code);
	}

	return transpStrip;
}

void Gdi::decompressMaskImg(byte *dst, const byte *src, int height) const {
	byte b, c;

	while (height) {
		b = *src++;

		if (b & 0x80) {
			b &= 0x7F;
			c = *src++;

			do {
				*dst = c;
				dst += _numStrips;
				--height;
			} while (--b && height);
		} else {
			do {
				*dst = *src++;
				dst += _numStrips;
				--height;
			} while (--b && height);
		}
	}
}

void Gdi::decompressTMSK(byte *dst, const byte *tmsk, const byte *src, int height) const {
	byte srcbits = 0;
	byte srcFlag = 0;
	byte maskFlag = 0;

	byte srcCount = 0;
	byte maskCount = 0;
	byte maskbits = 0;

	while (height) {
		if (srcCount == 0) {
			srcCount = *src++;
			srcFlag = srcCount & 0x80;
			if (srcFlag) {
				srcCount &= 0x7F;
				srcbits = *src++;
			}
		}

		if (srcFlag == 0) {
			srcbits = *src++;
		}

		srcCount--;

		if (maskCount == 0) {
			maskCount = *tmsk++;
			maskFlag = maskCount & 0x80;
			if (maskFlag) {
				maskCount &= 0x7F;
				maskbits = *tmsk++;
			}
		}

		if (maskFlag == 0) {
			maskbits = *tmsk++;
		}

		maskCount--;

		*dst |= srcbits;
		*dst &= ~maskbits;

		dst += _numStrips;
		height--;
	}
}

void Gdi::decompressMaskImgOr(byte *dst, const byte *src, int height) const {
	byte b, c;

	while (height) {
		b = *src++;

		if (b & 0x80) {
			b &= 0x7F;
			c = *src++;

			do {
				*dst |= c;
				dst += _numStrips;
				--height;
			} while (--b && height);
		} else {
			do {
				*dst |= *src++;
				dst += _numStrips;
				--height;
			} while (--b && height);
		}
	}
}

static void decodeNESTileData(const byte *src, byte *dest) {
	int len = READ_LE_UINT16(src);	src += 2;
	const byte *end = src + len;
	src++;	// skip number-of-tiles byte, assume it is correct
	while (src < end) {
		byte data = *src++;
		for (int j = 0; j < (data & 0x7F); j++)
			*dest++ = (data & 0x80) ? (*src++) : (*src);
		if (!(data & 0x80))
			src++;
	}
}

void ScummEngine::decodeNESBaseTiles() {
	byte *basetiles = getResourceAddress(rtCostume, 37);
	_NESBaseTiles = basetiles[2];
	decodeNESTileData(basetiles, _NESPatTable[1]);
}

static const int v1MMNEScostTables[2][6] = {
     /* desc lens offs data  gfx  pal */
	{ 25,  27,  29,  31,  33,  35},
	{ 26,  28,  30,  32,  34,  36}
};

void ScummEngine::NES_loadCostumeSet(int n) {
	int i;
	_NESCostumeSet = n;

	_NEScostdesc = getResourceAddress(rtCostume, v1MMNEScostTables[n][0]) + 2;
	_NEScostlens = getResourceAddress(rtCostume, v1MMNEScostTables[n][1]) + 2;
	_NEScostoffs = getResourceAddress(rtCostume, v1MMNEScostTables[n][2]) + 2;
	_NEScostdata = getResourceAddress(rtCostume, v1MMNEScostTables[n][3]) + 2;
	decodeNESTileData(getResourceAddress(rtCostume, v1MMNEScostTables[n][4]), _NESPatTable[0]);
	byte *palette = getResourceAddress(rtCostume, v1MMNEScostTables[n][5]) + 2;
	for (i = 0; i < 16; i++) {
		byte c = *palette++;
		if (c == 0x1D)	// HACK - switch around colors 0x00 and 0x1D
			c = 0;		// so we don't need a zillion extra checks
		else if (c == 0)// for determining the proper background color
			c = 0x1D;
		_NESPalette[1][i] = c;
	}

}

void GdiNES::decodeNESGfx(const byte *room) {
	const byte *gdata = room + READ_LE_UINT16(room + 0x0A);
	int tileset = *gdata++;
	int width = READ_LE_UINT16(room + 0x04);
	// int height = READ_LE_UINT16(room + 0x06);
	int i, j, n;

	// We have narrow room. so expand it
	if (width < 32) {
		_vm->_NESStartStrip = (32 - width) >> 1;
	} else {
		_vm->_NESStartStrip = 0;
	}

	decodeNESTileData(_vm->getResourceAddress(rtCostume, 37 + tileset), _vm->_NESPatTable[1] + _vm->_NESBaseTiles * 16);
	for (i = 0; i < 16; i++) {
		byte c = *gdata++;
		if (c == 0x0D)
			c = 0x1D;

		if (c == 0x1D)	 // HACK - switch around colors 0x00 and 0x1D
			c = 0;		 // so we don't need a zillion extra checks
		else if (c == 0) // for determining the proper background color
			c = 0x1D;

		_vm->_NESPalette[0][i] = c;
	}
	for (i = 0; i < 16; i++) {
		_NES.nametable[i][0] = _NES.nametable[i][1] = 0;
		n = 0;
		while (n < width) {
			byte data = *gdata++;
			for (j = 0; j < (data & 0x7F); j++)
				_NES.nametable[i][2 + n++] = (data & 0x80) ? (*gdata++) : (*gdata);
			if (!(data & 0x80))
				gdata++;
		}
		_NES.nametable[i][width+2] = _NES.nametable[i][width+3] = 0;
	}
	memcpy(_NES.nametableObj,_NES.nametable, 16*64);

	const byte *adata = room + READ_LE_UINT16(room + 0x0C);
	for (n = 0; n < 64;) {
		byte data = *adata++;
		for (j = 0; j < (data & 0x7F); j++)
			_NES.attributes[n++] = (data & 0x80) ? (*adata++) : (*adata);
		if (!(n & 7) && (width == 0x1C))
			n += 8;
		if (!(data & 0x80))
			adata++;
	}
	memcpy(_NES.attributesObj, _NES.attributes, 64);

	const byte *mdata = room + READ_LE_UINT16(room + 0x0E);
	int mask = *mdata++;
	if (mask == 0) {
		_NES.hasmask = false;
		return;
	}
	_NES.hasmask = true;
	if (mask != 1)
		debug(0,"NES room %i has irregular mask count %i",_vm->_currentRoom,mask);
	int mwidth = *mdata++;
	for (i = 0; i < 16; i++) {
		n = 0;
		while (n < mwidth) {
			byte data = *mdata++;
			for (j = 0; j < (data & 0x7F); j++)
				_NES.masktable[i][n++] = (data & 0x80) ? (*mdata++) : (*mdata);
			if (!(data & 0x80))
				mdata++;
		}
	}
	memcpy(_NES.masktableObj, _NES.masktable, 16*8);
}

void GdiNES::decodeNESObject(const byte *ptr, int xpos, int ypos, int width, int height) {
	int x, y;

	_NES.objX = xpos;

	// decode tile update data
	width /= 8;
	ypos /= 8;
	height /= 8;

	for (y = ypos; y < ypos + height; y++) {
		x = xpos;
		while (x < xpos + width) {
			byte len = *ptr++;
			for (int i = 0; i < (len & 0x7F); i++)
				_NES.nametableObj[y][2 + x++] = (len & 0x80) ? (*ptr++) : (*ptr);
			if (!(len & 0x80))
				ptr++;
		}
	}

	int ax, ay;
	// decode attribute update data
	y = height / 2;
	ay = ypos;
	while (y) {
		ax = xpos + 2;
		x = 0;
		int adata = 0;
		while (x < (width >> 1)) {
			if (!(x & 3))
				adata = *ptr++;
			byte *dest = &_NES.attributesObj[((ay << 2) & 0x30) | ((ax >> 2) & 0xF)];

			int aand = 3;
			int aor = adata & 3;
			if (ay & 0x02) {
				aand <<= 4;
				aor <<= 4;
			}
			if (ax & 0x02) {
				aand <<= 2;
				aor <<= 2;
			}
			*dest = ((~aand) & *dest) | aor;

			adata >>= 2;
			ax += 2;
			x++;
		}
		ay += 2;
		y--;
	}

	// decode mask update data
	if (!_NES.hasmask)
		return;
	int mx, mwidth;
	int lmask, rmask;
	mx = *ptr++;
	mwidth = *ptr++;
	lmask = *ptr++;
	rmask = *ptr++;

	for (y = 0; y < height; ++y) {
		byte *dest = &_NES.masktableObj[y + ypos][mx];
		*dest = (*dest & lmask) | *ptr++;
		dest++;
		for (x = 1; x < mwidth; x++) {
			if (x + 1 == mwidth)
				*dest = (*dest & rmask) | *ptr++;
			else
				*dest = *ptr++;
			dest++;
		}
	}
}

void GdiNES::drawStripNES(byte *dst, byte *mask, int dstPitch, int stripnr, int top, int height) {
	top /= 8;
	height /= 8;
	int x = stripnr + 2;	// NES version has a 2 tile gap on each edge

	if (_objectMode)
		x += _NES.objX; // for objects, need to start at the left edge of the object, not the screen
	if (x > 63) {
		debug(0,"NES tried to render invalid strip %i",stripnr);
		return;
	}
	for (int y = top; y < top + height; y++) {
		int palette = ((_objectMode ? _NES.attributesObj : _NES.attributes)[((y << 2) & 0x30) | ((x >> 2) & 0xF)] >> (((y & 2) << 1) | (x & 2))) & 0x3;
		int tile = (_objectMode ? _NES.nametableObj : _NES.nametable)[y][x];

		for (int i = 0; i < 8; i++) {
			byte c0 = _vm->_NESPatTable[1][tile * 16 + i];
			byte c1 = _vm->_NESPatTable[1][tile * 16 + i + 8];
			for (int j = 0; j < 8; j++)
				dst[j] = _vm->_NESPalette[0][((c0 >> (7 - j)) & 1) | (((c1 >> (7 - j)) & 1) << 1) | (palette << 2)];
			dst += dstPitch;
			*mask = c0 | c1;
			mask += _numStrips;
		}
	}
}

void GdiNES::drawStripNESMask(byte *dst, int stripnr, int top, int height) const {
	top /= 8;
	height /= 8;
	int x = stripnr;	// masks, unlike room graphics, should NOT be adjusted

	if (_objectMode)
		x += _NES.objX; // for objects, need to start at the left edge of the object, not the screen
	if (x > 63) {
		debug(0,"NES tried to mask invalid strip %i",stripnr);
		return;
	}
	for (int y = top; y < top + height; y++) {
		byte c;
		if (_NES.hasmask)
			c = (((_objectMode ? _NES.masktableObj : _NES.masktable)[y][x >> 3] >> (x & 7)) & 1) ? 0xFF : 0x00;
		else
			c = 0;

		for (int i = 0; i < 8; i++) {
			*dst &= c;
			dst += _numStrips;
		}
	}
}

void GdiV1::drawStripC64Background(byte *dst, int dstPitch, int stripnr, int height) {
	int charIdx;
	height /= 8;
	for (int y = 0; y < height; y++) {
		_C64.colors[3] = (_C64.colorMap[y + stripnr * height] & 7);
		// Check for room color change in V1 zak
		if (_roomPalette[0] == 255) {
			_C64.colors[2] = _roomPalette[2];
			_C64.colors[1] = _roomPalette[1];
		}

		charIdx = _C64.picMap[y + stripnr * height] * 8;
		for (int i = 0; i < 8; i++) {
			byte c = _C64.charMap[charIdx + i];
			dst[0] = dst[1] = _C64.colors[(c >> 6) & 3];
			dst[2] = dst[3] = _C64.colors[(c >> 4) & 3];
			dst[4] = dst[5] = _C64.colors[(c >> 2) & 3];
			dst[6] = dst[7] = _C64.colors[(c >> 0) & 3];
			dst += dstPitch;
		}
	}
}

void GdiV1::drawStripC64Object(byte *dst, int dstPitch, int stripnr, int width, int height) {
	int charIdx;
	height /= 8;
	width /= 8;
	for (int y = 0; y < height; y++) {
		_C64.colors[3] = (_C64.objectMap[(y + height) * width + stripnr] & 7);
		charIdx = _C64.objectMap[y * width + stripnr] * 8;
		for (int i = 0; i < 8; i++) {
			byte c = _C64.charMap[charIdx + i];
			dst[0] = dst[1] = _C64.colors[(c >> 6) & 3];
			dst[2] = dst[3] = _C64.colors[(c >> 4) & 3];
			dst[4] = dst[5] = _C64.colors[(c >> 2) & 3];
			dst[6] = dst[7] = _C64.colors[(c >> 0) & 3];
			dst += dstPitch;
		}
	}
}

void GdiV1::drawStripC64Mask(byte *dst, int stripnr, int width, int height) const {
	int maskIdx;
	height /= 8;
	width /= 8;
	for (int y = 0; y < height; y++) {
		if (_objectMode)
			maskIdx = _C64.objectMap[(y + 2 * height) * width + stripnr] * 8;
		else
			maskIdx = _C64.maskMap[y + stripnr * height] * 8;
		for (int i = 0; i < 8; i++) {
			byte c = _C64.maskChar[maskIdx + i];

			// V1/C64 masks are inverted compared to what ScummVM expects
			*dst = c ^ 0xFF;
			dst += _numStrips;
		}
	}
}

void GdiV1::decodeC64Gfx(const byte *src, byte *dst, int size) const {
	int x, z;
	byte color, run, common[4];

	for (z = 0; z < 4; z++) {
		common[z] = *src++;
	}

	x = 0;
	while (x < size) {
		run = *src++;
		if (run & 0x80) {
			color = common[(run >> 5) & 3];
			run &= 0x1F;
			for (z = 0; z <= run; z++) {
				dst[x++] = color;
			}
		} else if (run & 0x40) {
			run &= 0x3F;
			color = *src++;
			for (z = 0; z <= run; z++) {
				dst[x++] = color;
			}
		} else {
			for (z = 0; z <= run; z++) {
				dst[x++] = *src++;
			}
		}
	}
}

/**
 * Create and fill a table with offsets to the graphic and mask strips in the
 * given V2 EGA bitmap.
 * @param src		the V2 EGA bitmap
 * @param width		the width of the bitmap
 * @param height	the height of the bitmap
 * @param table		the strip table to fill
 * @return filled strip table
 */
StripTable *GdiV2::generateStripTable(const byte *src, int width, int height, StripTable *table) const {

	// If no strip table was given to use, allocate a new one
	if (table == 0)
		table = (StripTable *)calloc(1, sizeof(StripTable));

	const byte *bitmapStart = src;
	byte color = 0, data = 0;
	int x, y, length = 0;
	byte run = 1;

	// Decode the graphics strips, and memorize the run/color values
	// as well as the byte offset.
	for (x = 0 ; x < width; x++) {

		if ((x % 8) == 0) {
			assert(x / 8 < 160);
			table->run[x / 8] = run;
			table->color[x / 8] = color;
			table->offsets[x / 8] = src - bitmapStart;
		}

		for (y = 0; y < height; y++) {
			if (--run == 0) {
				data = *src++;
				if (data & 0x80) {
					run = data & 0x7f;
				} else {
					run = data >> 4;
				}
				if (run == 0) {
					run = *src++;
				}
				color = data & 0x0f;
			}
		}
	}

	// The mask data follows immediately after the graphics.
	x = 0;
	y = height;
	width /= 8;

	for (;;) {
		length = *src++;
		const byte runFlag = length & 0x80;
		if (runFlag) {
			length &= 0x7f;
			data = *src++;
		}
		do {
			if (!runFlag)
				data = *src++;
			if (y == height) {
				assert(x < 120);
				table->zoffsets[x] = src - bitmapStart - 1;
				table->zrun[x] = length | runFlag;
			}
			if (--y == 0) {
				if (--width == 0)
					return table;
				x++;
				y = height;
			}
		} while (--length);
	}

	return table;
}

void Gdi::drawStripEGA(byte *dst, int dstPitch, const byte *src, int height) const {
	byte color = 0;
	int run = 0, x = 0, y = 0, z;

	while (x < 8) {
		color = *src++;

		if (color & 0x80) {
			run = color & 0x3f;

			if (color & 0x40) {
				color = *src++;

				if (run == 0) {
					run = *src++;
				}
				for (z = 0; z < run; z++) {
					*(dst + y * dstPitch + x) = (z & 1) ? _roomPalette[color & 0xf] + _paletteMod : _roomPalette[color >> 4] + _paletteMod;

					y++;
					if (y >= height) {
						y = 0;
						x++;
					}
				}
			} else {
				if (run == 0) {
					run = *src++;
				}

				for (z = 0; z < run; z++) {
					*(dst + y * dstPitch + x) = *(dst + y * dstPitch + x - 1);

					y++;
					if (y >= height) {
						y = 0;
						x++;
					}
				}
			}
		} else {
			run = color >> 4;
			if (run == 0) {
				run = *src++;
			}

			for (z = 0; z < run; z++) {
				*(dst + y * dstPitch + x) = _roomPalette[color & 0xf] + _paletteMod;

				y++;
				if (y >= height) {
					y = 0;
					x++;
				}
			}
		}
	}
}

#define READ_BIT (shift--, dataBit = data & 1, data >>= 1, dataBit)
#define FILL_BITS(n) do {            \
		if (shift < n) {             \
			data |= *src++ << shift; \
			shift += 8;              \
		}                            \
	} while (0)

// NOTE: drawStripHE is actually very similar to drawStripComplex
void Gdi::drawStripHE(byte *dst, int dstPitch, const byte *src, int width, int height, const bool transpCheck) const {
	static const int delta_color[] = { -4, -3, -2, -1, 1, 2, 3, 4 };
	uint32 dataBit, data;
	byte color;
	int shift;

	color = *src++;
	data = READ_LE_UINT24(src);
	src += 3;
	shift = 24;

	int x = width;
	while (1) {
		if (!transpCheck || color != _transparentColor)
			*dst = _roomPalette[color];
		dst++;
		--x;
		if (x == 0) {
			x = width;
			dst += dstPitch - width;
			--height;
			if (height == 0)
				return;
		}
		FILL_BITS(1);
		if (READ_BIT) {
			FILL_BITS(1);
			if (READ_BIT) {
				FILL_BITS(3);
				color += delta_color[data & 7];
				shift -= 3;
				data >>= 3;
			} else {
				FILL_BITS(_decomp_shr);
				color = data & _decomp_mask;
				shift -= _decomp_shr;
				data >>= _decomp_shr;
			}
		}
	}
}

#undef READ_BIT
#undef FILL_BITS


void Gdi::drawStrip3DO(byte *dst, int dstPitch, const byte *src, int height, const bool transpCheck) const {
	if (height == 0)
		return;

	int decSize = height * 8;
	int curSize = 0;

	do {
		uint8 data = *src++;
		uint8 rle = data & 1;
		int len = (data >> 1) + 1;

		len = MIN(decSize, len);
		decSize -= len;

		if (!rle) {
			for (; len > 0; len--, src++, dst++) {
				if (!transpCheck || *src != _transparentColor)
					*dst = _roomPalette[*src];
				curSize++;
				if (!(curSize & 7))
					dst += dstPitch - 8; // Next row
			}
		} else {
			byte color = *src++;
			for (; len > 0; len--, dst++) {
				if (!transpCheck || color != _transparentColor)
					*dst = _roomPalette[color];
				curSize++;
				if (!(curSize & 7))
					dst += dstPitch - 8; // Next row
			}
		}
	} while (decSize > 0);
}


#define READ_BIT (cl--, bit = bits & 1, bits >>= 1, bit)
#define FILL_BITS do {              \
		if (cl <= 8) {              \
			bits |= (*src++ << cl); \
			cl += 8;                \
		}                           \
	} while (0)

void Gdi::drawStripComplex(byte *dst, int dstPitch, const byte *src, int height, const bool transpCheck) const {
	byte color = *src++;
	uint bits = *src++;
	byte cl = 8;
	byte bit;
	byte incm, reps;

	do {
		int x = 8;
		do {
			FILL_BITS;
			if (!transpCheck || color != _transparentColor)
				*dst = _roomPalette[color] + _paletteMod;
			dst++;

		againPos:
			if (!READ_BIT) {
			} else if (!READ_BIT) {
				FILL_BITS;
				color = bits & _decomp_mask;
				bits >>= _decomp_shr;
				cl -= _decomp_shr;
			} else {
				incm = (bits & 7) - 4;
				cl -= 3;
				bits >>= 3;
				if (incm) {
					color += incm;
				} else {
					FILL_BITS;
					reps = bits & 0xFF;
					do {
						if (!--x) {
							x = 8;
							dst += dstPitch - 8;
							if (!--height)
								return;
						}
						if (!transpCheck || color != _transparentColor)
							*dst = _roomPalette[color] + _paletteMod;
						dst++;
					} while (--reps);
					bits >>= 8;
					bits |= (*src++) << (cl - 8);
					goto againPos;
				}
			}
		} while (--x);
		dst += dstPitch - 8;
	} while (--height);
}

void Gdi::drawStripBasicH(byte *dst, int dstPitch, const byte *src, int height, const bool transpCheck) const {
	byte color = *src++;
	uint bits = *src++;
	byte cl = 8;
	byte bit;
	int8 inc = -1;

	do {
		int x = 8;
		do {
			FILL_BITS;
			if (!transpCheck || color != _transparentColor)
				*dst = _roomPalette[color] + _paletteMod;
			dst++;
			if (!READ_BIT) {
			} else if (!READ_BIT) {
				FILL_BITS;
				color = bits & _decomp_mask;
				bits >>= _decomp_shr;
				cl -= _decomp_shr;
				inc = -1;
			} else if (!READ_BIT) {
				color += inc;
			} else {
				inc = -inc;
				color += inc;
			}
		} while (--x);
		dst += dstPitch - 8;
	} while (--height);
}

void Gdi::drawStripBasicV(byte *dst, int dstPitch, const byte *src, int height, const bool transpCheck) const {
	byte color = *src++;
	uint bits = *src++;
	byte cl = 8;
	byte bit;
	int8 inc = -1;

	int x = 8;
	do {
		int h = height;
		do {
			FILL_BITS;
			if (!transpCheck || color != _transparentColor)
				*dst = _roomPalette[color] + _paletteMod;
			dst += dstPitch;
			if (!READ_BIT) {
			} else if (!READ_BIT) {
				FILL_BITS;
				color = bits & _decomp_mask;
				bits >>= _decomp_shr;
				cl -= _decomp_shr;
				inc = -1;
			} else if (!READ_BIT) {
				color += inc;
			} else {
				inc = -inc;
				color += inc;
			}
		} while (--h);
		dst -= _vertStripNextInc;
	} while (--x);
}

#undef READ_BIT
#undef FILL_BITS

/* Ender - Zak256/Indy256 decoders */
#define READ_BIT_256                       \
		do {                               \
			if ((mask <<= 1) == 256) {     \
				buffer = *src++;           \
				mask = 1;                  \
			}                              \
			bits = ((buffer & mask) != 0); \
		} while (0)

#define READ_N_BITS(n, c)                  \
		do {                               \
			c = 0;                         \
			for (int b = 0; b < n; b++) {  \
				READ_BIT_256;              \
				c += (bits << b);          \
			}                              \
		} while (0)

#define NEXT_ROW                           \
		do {                               \
			dst += dstPitch;               \
			if (--h == 0) {                \
				if (!--x)                  \
					return;                \
				dst -= _vertStripNextInc;  \
				h = height;                \
			}                              \
		} while (0)

void Gdi::drawStripRaw(byte *dst, int dstPitch, const byte *src, int height, const bool transpCheck) const {
	int x;

	if (_vm->_game.features & GF_OLD256) {
		uint h = height;
		x = 8;
		for (;;) {
			*dst = _roomPalette[*src++];
			NEXT_ROW;
		}
	} else {
		do {
			for (x = 0; x < 8; x ++) {
				byte color = *src++;
				if (!transpCheck || color != _transparentColor)
					dst[x] = _roomPalette[color] + _paletteMod;
			}
			dst += dstPitch;
		} while (--height);
	}
}

void Gdi::unkDecode8(byte *dst, int dstPitch, const byte *src, int height) const {
	uint h = height;

	int x = 8;
	for (;;) {
		uint run = (*src++) + 1;
		byte color = *src++;

		do {
			*dst = _roomPalette[color];
			NEXT_ROW;
		} while (--run);
	}
}

void Gdi::unkDecode9(byte *dst, int dstPitch, const byte *src, int height) const {
	byte c, bits, color, run;
	int i;
	uint buffer = 0, mask = 128;
	int h = height;
	i = run = 0;

	int x = 8;
	for (;;) {
		READ_N_BITS(4, c);

		switch (c >> 2) {
		case 0:
			READ_N_BITS(4, color);
			for (i = 0; i < ((c & 3) + 2); i++) {
				*dst = _roomPalette[run * 16 + color];
				NEXT_ROW;
			}
			break;

		case 1:
			for (i = 0; i < ((c & 3) + 1); i++) {
				READ_N_BITS(4, color);
				*dst = _roomPalette[run * 16 + color];
				NEXT_ROW;
			}
			break;

		case 2:
			READ_N_BITS(4, run);
			break;
		}
	}
}

void Gdi::unkDecode10(byte *dst, int dstPitch, const byte *src, int height) const {
	int i;
	byte local_palette[256], numcolors = *src++;
	uint h = height;

	for (i = 0; i < numcolors; i++)
		local_palette[i] = *src++;

	int x = 8;

	for (;;) {
		byte color = *src++;
		if (color < numcolors) {
			*dst = _roomPalette[local_palette[color]];
			NEXT_ROW;
		} else {
			uint run = color - numcolors + 1;
			color = *src++;
			do {
				*dst = _roomPalette[color];
				NEXT_ROW;
			} while (--run);
		}
	}
}


void Gdi::unkDecode11(byte *dst, int dstPitch, const byte *src, int height) const {
	int bits, i;
	uint buffer = 0, mask = 128;
	byte inc = 1, color = *src++;

	int x = 8;
	do {
		int h = height;
		do {
			*dst = _roomPalette[color];
			dst += dstPitch;
			for (i = 0; i < 3; i++) {
				READ_BIT_256;
				if (!bits)
					break;
			}
			switch (i) {
			case 1:
				inc = -inc;
				color -= inc;
				break;

			case 2:
				color -= inc;
				break;

			case 3:
				inc = 1;
				READ_N_BITS(8, color);
				break;
			}
		} while (--h);
		dst -= _vertStripNextInc;
	} while (--x);
}

#undef NEXT_ROW
#undef READ_BIT_256


#pragma mark -
#pragma mark --- Transition effects ---
#pragma mark -

void ScummEngine::fadeIn(int effect) {
	if (_disableFadeInEffect) {
		// fadeIn() calls can be disabled in TheDig after a SMUSH movie
		// has been played. Like the original interpreter, we introduce
		// an extra flag to handle this.
		_disableFadeInEffect = false;
		_doEffect = false;
		_screenEffectFlag = true;
		return;
	}

	updatePalette();

	switch (effect) {
	case 0:
		// seems to do nothing
		break;
	case 1:
	case 2:
	case 3:
	case 4:
	case 5:
	case 6:
		// Some of the transition effects won't work properly unless
		// the screen is marked as clean first. At first I thought I
		// could safely do this every time fadeIn() was called, but
		// that broke the FOA intro. Probably other things as well.
		//
		// Hopefully it's safe to do it at this point, at least.
		_virtscr[kMainVirtScreen].setDirtyRange(0, 0);
		transitionEffect(effect - 1);
		break;
	case 128:
		unkScreenEffect6();
		break;
	case 129:
		break;
	case 130:
	case 131:
	case 132:
	case 133:
		scrollEffect(133 - effect);
		break;
	case 134:
		dissolveEffect(1, 1);
		break;
	case 135:
		dissolveEffect(1, _virtscr[kMainVirtScreen].h);
		break;
	default:
		error("Unknown screen effect, %d", effect);
	}
	_screenEffectFlag = true;
}

void ScummEngine::fadeOut(int effect) {
	VirtScreen *vs = &_virtscr[kMainVirtScreen];

	vs->setDirtyRange(0, 0);
	if (_game.version < 7)
		camera._last.x = camera._cur.x;

	// TheDig can disable fadeIn(), and may call fadeOut() several times
	// successively. Disabling the _screenEffectFlag check forces the screen
	// to get cleared. This fixes glitches, at least, in the first cutscenes
	// when bypassed of FT and TheDig.
	if ((_game.version == 7 || _screenEffectFlag) && effect != 0) {

		// Fill screen 0 with black
		memset(vs->getPixels(0, 0), 0, vs->pitch * vs->h);

		// Fade to black with the specified effect, if any.
		switch (effect) {
		case 1:
		case 2:
		case 3:
		case 4:
		case 5:
		case 6:
			transitionEffect(effect - 1);
			break;
		case 128:
			unkScreenEffect6();
			break;
		case 129:
			// Just blit screen 0 to the display (i.e. display will be black)
			vs->setDirtyRange(0, vs->h);
			updateDirtyScreen(kMainVirtScreen);
			break;
		case 134:
			dissolveEffect(1, 1);
			break;
		case 135:
			dissolveEffect(1, _virtscr[kMainVirtScreen].h);
			break;
		default:
			error("fadeOut: default case %d", effect);
		}
	}

	// Update the palette at the end (once we faded to black) to avoid
	// some nasty effects when the palette is changed
	updatePalette();

	_screenEffectFlag = false;
}

/**
 * Perform a transition effect. There are four different effects possible:
 * 0: Iris effect
 * 1: Box wipe (a black box expands from the upper-left corner to the lower-right corner)
 * 2: Box wipe (a black box expands from the lower-right corner to the upper-left corner)
 * 3: Inverse box wipe
 * All effects operate on 8x8 blocks of the screen. These blocks are updated
 * in a certain order; the exact order determines how the effect appears to the user.
 * @param a		the transition effect to perform
 */
void ScummEngine::transitionEffect(int a) {
	int delta[16];								// Offset applied during each iteration
	int tab_2[16];
	int i, j;
	int bottom;
	int l, t, r, b;
	const int height = MIN((int)_virtscr[kMainVirtScreen].h, _screenHeight);
	const int delay = (VAR_FADE_DELAY != 0xFF) ? VAR(VAR_FADE_DELAY) * kFadeDelay : kPictureDelay;

	for (i = 0; i < 16; i++) {
		delta[i] = transitionEffects[a].deltaTable[i];
		j = transitionEffects[a].stripTable[i];
		if (j == 24)
			j = height / 8 - 1;
		tab_2[i] = j;
	}

	bottom = height / 8;
	for (j = 0; j < transitionEffects[a].numOfIterations; j++) {
		for (i = 0; i < 4; i++) {
			l = tab_2[i * 4];
			t = tab_2[i * 4 + 1];
			r = tab_2[i * 4 + 2];
			b = tab_2[i * 4 + 3];

			if (t == b) {
				while (l <= r) {
					if (l >= 0 && l < _gdi->_numStrips && t < bottom) {
						_virtscr[kMainVirtScreen].tdirty[l] = _screenTop + t * 8;
						_virtscr[kMainVirtScreen].bdirty[l] = _screenTop + (b + 1) * 8;
					}
					l++;
				}
			} else {
				if (l < 0 || l >= _gdi->_numStrips || b <= t)
					continue;
				if (b > bottom)
					b = bottom;
				if (t < 0)
					t = 0;
				_virtscr[kMainVirtScreen].tdirty[l] = _screenTop + t * 8;
				_virtscr[kMainVirtScreen].bdirty[l] = _screenTop + (b + 1) * 8;
			}
			updateDirtyScreen(kMainVirtScreen);
		}

		for (i = 0; i < 16; i++)
			tab_2[i] += delta[i];

		// Draw the current state to the screen and wait a few secs so the
		// user can watch the effect taking place.
		waitForTimer(delay);
	}
}

/**
 * Update width*height areas of the screen, in random order, until the whole
 * screen has been updated. For instance:
 *
 * dissolveEffect(1, 1) produces a pixel-by-pixel dissolve
 * dissolveEffect(8, 8) produces a square-by-square dissolve
 * dissolveEffect(virtsrc[0].width, 1) produces a line-by-line dissolve
 */
void ScummEngine::dissolveEffect(int width, int height) {
	VirtScreen *vs = &_virtscr[kMainVirtScreen];
	int *offsets;
	int blits_before_refresh, blits;
	int x, y;
	int w, h;
	int i;

	// There's probably some less memory-hungry way of doing this. But
	// since we're only dealing with relatively small images, it shouldn't
	// be too bad.

	w = vs->w / width;
	h = vs->h / height;

	// When used correctly, vs->width % width and vs->height % height
	// should both be zero, but just to be safe...

	if (vs->w % width)
		w++;

	if (vs->h % height)
		h++;

	offsets = (int *) malloc(w * h * sizeof(int));
	if (offsets == NULL)
		error("dissolveEffect: out of memory");

	// Create a permutation of offsets into the frame buffer

	if (width == 1 && height == 1) {
		// Optimized case for pixel-by-pixel dissolve

		for (i = 0; i < vs->w * vs->h; i++)
			offsets[i] = i;

		for (i = 1; i < w * h; i++) {
			int j;

			j = _rnd.getRandomNumber(i - 1);
			offsets[i] = offsets[j];
			offsets[j] = i;
		}
	} else {
		int *offsets2;

		for (i = 0, x = 0; x < vs->w; x += width)
			for (y = 0; y < vs->h; y += height)
				offsets[i++] = y * vs->pitch + x;

		offsets2 = (int *) malloc(w * h * sizeof(int));
		if (offsets2 == NULL)
			error("dissolveEffect: out of memory");

		memcpy(offsets2, offsets, w * h * sizeof(int));

		for (i = 1; i < w * h; i++) {
			int j;

			j = _rnd.getRandomNumber(i - 1);
			offsets[i] = offsets[j];
			offsets[j] = offsets2[i];
		}

		free(offsets2);
	}

	// Blit the image piece by piece to the screen. The idea here is that
	// the whole update should take about a quarter of a second, assuming
	// most of the time is spent in waitForTimer(). It looks good to me,
	// but might still need some tuning.

	blits = 0;
	blits_before_refresh = (3 * w * h) / 25;

	// Speed up the effect for CD Loom since it uses it so often. I don't
	// think the original had any delay at all, so on modern hardware it
	// wasn't even noticeable.
	if (_game.id == GID_LOOM && (_game.version == 4))
		blits_before_refresh *= 2;

	for (i = 0; i < w * h; i++) {
		x = offsets[i] % vs->pitch;
		y = offsets[i] / vs->pitch;

		if (_useCJKMode && _textSurfaceMultiplier == 2) {
			int m = _textSurfaceMultiplier;
			byte *dst = _fmtownsBuf + x * m + y * m * _screenWidth * m;
			scale2x(dst, _screenWidth * m, vs->getPixels(x, y), vs->pitch,  width, height);

			_system->copyRectToScreen(dst, _screenWidth * m, x * m, (y + vs->topline) * m, width * m, height * m);
		} else {
			_system->copyRectToScreen(vs->getPixels(x, y), vs->pitch, x, y + vs->topline, width, height);
		}


		if (++blits >= blits_before_refresh) {
			blits = 0;
			waitForTimer(30);
		}
	}

	free(offsets);

	if (blits != 0) {
		waitForTimer(30);
	}
}

void ScummEngine::scrollEffect(int dir) {
	VirtScreen *vs = &_virtscr[kMainVirtScreen];

	int x, y;
	int step;
	const int delay = (VAR_FADE_DELAY != 0xFF) ? VAR(VAR_FADE_DELAY) * kFadeDelay : kPictureDelay;

	if ((dir == 0) || (dir == 1))
		step = vs->h;
	else
		step = vs->w;

	step = (step * delay) / kScrolltime;

	byte *src;
	int m = _textSurfaceMultiplier;
	int vsPitch = vs->pitch;

	switch (dir) {
	case 0:
		//up
		y = 1 + step;
		while (y < vs->h) {
			moveScreen(0, -step, vs->h);

			src = vs->getPixels(0, y - step);
			if (_useCJKMode && m == 2) {
				int x1 = 0, y1 = vs->h - step;
				byte *dst = _fmtownsBuf + x1 * m + y1 * m * _screenWidth * m;
				scale2x(dst, _screenWidth * m, src, vs->pitch, vs->w, step);
				src = dst;
				vsPitch = _screenWidth * 2;
			}

			_system->copyRectToScreen(src,
				vsPitch,
				0 * m, (vs->h - step) * m,
				vs->w * m, step * m);
			_system->updateScreen();
			waitForTimer(delay);

			y += step;
		}
		break;
	case 1:
		// down
		y = 1 + step;
		while (y < vs->h) {
			moveScreen(0, step, vs->h);
			src = vs->getPixels(0, vs->h - y);
			if (_useCJKMode && m == 2) {
				int x1 = 0, y1 = 0;
				byte *dst = _fmtownsBuf + x1 * m + y1 * m * _screenWidth * m;
				scale2x(dst, _screenWidth * m, src, vs->pitch, vs->w, step);
				src = dst;
				vsPitch = _screenWidth * 2;
			}
			_system->copyRectToScreen(src,
				vsPitch,
				0, 0,
				vs->w * m, step * m);
			_system->updateScreen();
			waitForTimer(delay);

			y += step;
		}
		break;
	case 2:
		// left
		x = 1 + step;
		while (x < vs->w) {
			moveScreen(-step, 0, vs->h);
			src = vs->getPixels(x - step, 0);
			if (_useCJKMode && m == 2) {
				int x1 = vs->w - step, y1 = 0;
				byte *dst = _fmtownsBuf + x1 * m + y1 * m * _screenWidth * m;
				scale2x(dst, _screenWidth * m, src, vs->pitch, step, vs->h);
				src = dst;
				vsPitch = _screenWidth * 2;
			}
			_system->copyRectToScreen(src,
				vsPitch,
				(vs->w - step) * m, 0,
				step * m, vs->h * m);
			_system->updateScreen();
			waitForTimer(delay);

			x += step;
		}
		break;
	case 3:
		// right
		x = 1 + step;
		while (x < vs->w) {
			moveScreen(step, 0, vs->h);
			src = vs->getPixels(vs->w - x, 0);
			if (_useCJKMode && m == 2) {
				int x1 = 0, y1 = 0;
				byte *dst = _fmtownsBuf + x1 * m + y1 * m * _screenWidth * m;
				scale2x(dst, _screenWidth * m, src, vs->pitch, step, vs->h);
				src = dst;
				vsPitch = _screenWidth * 2;
			}
			_system->copyRectToScreen(src,
				vsPitch,
				0, 0,
				step, vs->h);
			_system->updateScreen();
			waitForTimer(delay);

			x += step;
		}
		break;
	}
}

void ScummEngine::unkScreenEffect6() {
	// CD Loom (but not EGA Loom!) uses a more fine-grained dissolve
	if (_game.id == GID_LOOM && (_game.version == 4))
		dissolveEffect(1, 1);
	else
		dissolveEffect(8, 4);
}

} // End of namespace Scumm

