/* Copyright (C) 1994-2004 Revolution Software Ltd * * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * $Header$ */ #include "common/stdafx.h" #include "sword2/sword2.h" #include "sword2/driver/animation.h" #include "sword2/driver/d_draw.h" #include "sword2/driver/menu.h" #include "sword2/driver/render.h" namespace Sword2 { #define MILLISECSPERCYCLE 83 #define BLOCKWBITS 6 #define BLOCKHBITS 6 void Graphics::updateRect(Common::Rect *r) { _vm->_system->copyRectToScreen(_buffer + r->top * _screenWide + r->left, _screenWide, r->left, r->top, r->right - r->left, r->bottom - r->top); } void Graphics::blitBlockSurface(BlockSurface *s, Common::Rect *r, Common::Rect *clipRect) { if (!r->intersects(*clipRect)) return; byte *src = s->data; if (r->top < clipRect->top) { src -= BLOCKWIDTH * (r->top - clipRect->top); r->top = clipRect->top; } if (r->left < clipRect->left) { src -= (r->left - clipRect->left); r->left = clipRect->left; } if (r->bottom > clipRect->bottom) r->bottom = clipRect->bottom; if (r->right > clipRect->right) r->right = clipRect->right; byte *dst = _buffer + r->top * _screenWide + r->left; int i, j; if (s->transparent) { for (i = 0; i < r->bottom - r->top; i++) { for (j = 0; j < r->right - r->left; j++) { if (src[j]) dst[j] = src[j]; } src += BLOCKWIDTH; dst += _screenWide; } } else { for (i = 0; i < r->bottom - r->top; i++) { memcpy(dst, src, r->right - r->left); src += BLOCKWIDTH; dst += _screenWide; } } } // I've made the scaling two separate functions because there were cases from // DrawSprite() where it wasn't obvious if the sprite should grow or shrink, // which caused crashes. // // Keeping them separate might be a good idea anyway, for readability. // // The code is based on the original DrawSprite() code, so apart from not // knowing if I got it right, I don't know how good the original really is. // // The backbuf parameter points to the buffer where the image will eventually // be drawn. This is only used at the highest graphics detail setting (and not // always even then) and is used to help anti-alias the image. void Graphics::squashImage(byte *dst, uint16 dstPitch, uint16 dstWidth, uint16 dstHeight, byte *src, uint16 srcPitch, uint16 srcWidth, uint16 srcHeight, byte *backbuf) { int32 ince, incne, d; int16 x, y; // Work out the x-scale ince = 2 * dstWidth; incne = 2 * (dstWidth - srcWidth); d = 2 * dstWidth - srcWidth; x = y = 0; _xScale[y] = x; while (x < srcWidth) { if (d <= 0) { d += ince; x++; } else { d += incne; x++; y++; } _xScale[y] = x; } // Work out the y-scale ince = 2 * dstHeight; incne = 2 * (dstHeight - srcHeight); d = 2 * dstHeight - srcHeight; x = y = 0; _yScale[y] = x; while (x < srcHeight) { if (d <= 0) { d += ince; x++; } else { d += incne; x++; y++; } _yScale[y] = x; } // Copy the image (with or without anti-aliasing) if (backbuf) { for (y = 0; y < dstHeight; y++) { for (x = 0; x < dstWidth; x++) { uint8 p; uint8 p1 = 0; int count = 0; int spriteCount = 0; int red = 0; int green = 0; int blue = 0; int i, j; for (j = _yScale[y]; j < _yScale[y + 1]; j++) { for (i = _xScale[x]; i < _xScale[x + 1]; i++) { p = src[j * srcPitch + i]; if (p) { red += _palCopy[p][0]; green += _palCopy[p][1]; blue += _palCopy[p][2]; p1 = p; spriteCount++; } else { red += _palCopy[backbuf[x]][0]; green += _palCopy[backbuf[x]][1]; blue += _palCopy[backbuf[x]][2]; } count++; } } if (spriteCount == 0) dst[x] = 0; else if (spriteCount == 1) dst[x] = p1; else dst[x] = quickMatch((uint8) (red / count), (uint8) (green / count), (uint8) (blue / count)); } dst += dstPitch; backbuf += _screenWide; } } else { for (y = 0; y < dstHeight; y++) { for (x = 0; x < dstWidth; x++) { dst[x] = src[_yScale[y] * srcPitch + _xScale[x]]; } dst += dstPitch; } } } void Graphics::stretchImage(byte *dst, uint16 dstPitch, uint16 dstWidth, uint16 dstHeight, byte *src, uint16 srcPitch, uint16 srcWidth, uint16 srcHeight, byte *backbuf) { byte *origDst = dst; int32 ince, incne, d; int16 x, y, i, j, k; // Work out the x-scale ince = 2 * srcWidth; incne = 2 * (srcWidth - dstWidth); d = 2 * srcWidth - dstWidth; x = y = 0; _xScale[y] = x; while (x < dstWidth) { if (d <= 0) { d += ince; x++; } else { d += incne; x++; y++; _xScale[y] = x; } } // Work out the y-scale ince = 2 * srcHeight; incne = 2 * (srcHeight - dstHeight); d = 2 * srcHeight - dstHeight; x = y = 0; _yScale[y] = x; while (x < dstHeight) { if (d <= 0) { d += ince; x++; } else { d += incne; x++; y++; _yScale[y] = x; } } // Copy the image for (y = 0; y < srcHeight; y++) { for (j = _yScale[y]; j < _yScale[y + 1]; j++) { k = 0; for (x = 0; x < srcWidth; x++) { for (i = _xScale[x]; i < _xScale[x + 1]; i++) { dst[k++] = src[y * srcPitch + x]; } } dst += dstPitch; } } // Anti-aliasing if (backbuf) { byte *newDst = (byte *) malloc(dstWidth * dstHeight); if (!newDst) return; memcpy(newDst, origDst, dstWidth); for (y = 1; y < dstHeight - 1; y++) { src = origDst + y * dstPitch; dst = newDst + y * dstWidth; *dst++ = *src++; for (x = 1; x < dstWidth - 1; x++) { byte pt[5]; byte *p = backbuf + y * 640 + x; int count = 0; if (*src) { count++; pt[0] = *src; } else pt[0] = *p; pt[1] = *(src - dstPitch); if (pt[1] == 0) pt[1] = *(p - 640); else count++; pt[2] = *(src - 1); if (pt[2] == 0) pt[2] = *(p - 1); else count++; pt[3] = *(src + 1); if (pt[3] == 0) pt[3] = *(p + 1); else count++; pt[4] = *(src + dstPitch); if (pt[4] == 0) pt[4] = *(p + 640); else count++; if (count) { int red = _palCopy[pt[0]][0] << 2; int green = _palCopy[pt[0]][1] << 2; int blue = _palCopy[pt[0]][2] << 2; for (i = 1; i < 5; i++) { red += _palCopy[pt[i]][0]; green += _palCopy[pt[i]][1]; blue += _palCopy[pt[i]][2]; } *dst++ = quickMatch((uint8) (red >> 3), (uint8) (green >> 3), (uint8) (blue >> 3)); } else *dst++ = 0; src++; } *dst++ = *src++; } memcpy(dst, src, dstWidth); src = newDst; dst = origDst; for (i = 0; i < dstHeight; i++) { memcpy(dst, src, dstWidth); dst += dstPitch; src += dstWidth; } free(newDst); } } /** * Plots a point relative to the top left corner of the screen. This is only * used for debugging. * @param x x-coordinate of the point * @param y y-coordinate of the point * @param colour colour of the point */ void Graphics::plotPoint(uint16 x, uint16 y, uint8 colour) { byte *buf = _buffer + 40 * RENDERWIDE; int16 newx, newy; newx = x - _scrollX; newy = y - _scrollY; if (newx >= 0 && newx < RENDERWIDE && newy >= 0 && newy < RENDERDEEP) { buf[newy * RENDERWIDE + newx] = colour; markAsDirty(newx, newy + 40, newx, newy + 40); } } /** * Draws a line from one point to another. This is only used for debugging. * @param x0 x-coordinate of the start point * @param y0 y-coordinate of the start point * @param x1 x-coordinate of the end point * @param y1 y-coordinate of the end point * @param colour colour of the line */ // Uses Bressnham's incremental algorithm! void Graphics::drawLine(int16 x0, int16 y0, int16 x1, int16 y1, uint8 colour) { byte *buf = _buffer + 40 * RENDERWIDE; int dx, dy; int dxmod, dymod; int ince, incne; int d; int x, y; int addTo; x1 -= _scrollX; y1 -= _scrollY; x0 -= _scrollX; y0 -= _scrollY; markAsDirty(MIN(x0, x1), MIN(y0, y1) + 40, MAX(x0, x1), MAX(y0, y1) + 40); // Make sure we're going from left to right if (x1 < x0) { SWAP(x0, x1); SWAP(y0, y1); } dx = x1 - x0; dy = y1 - y0; if (dx < 0) dxmod = -dx; else dxmod = dx; if (dy < 0) dymod = -dy; else dymod = dy; if (dxmod >= dymod) { if (dy > 0) { d = 2 * dy - dx; ince = 2 * dy; incne = 2 * (dy - dx); x = x0; y = y0; if (x >= 0 && x < RENDERWIDE && y >= 0 && y < RENDERDEEP) buf[y * RENDERWIDE + x] = colour; while (x < x1) { if (d <= 0) { d += ince; x++; } else { d += incne; x++; y++; } if (x >= 0 && x < RENDERWIDE && y >= 0 && y < RENDERDEEP) buf[y * RENDERWIDE + x] = colour; } } else { addTo = y0; y0 = 0; y1 -= addTo; y1 = -y1; dy = y1 - y0; d = 2 * dy - dx; ince = 2 * dy; incne = 2 * (dy - dx); x = x0; y = y0; if (x >= 0 && x < RENDERWIDE && addTo - y >= 0 && addTo - y < RENDERDEEP) buf[(addTo - y) * RENDERWIDE + x] = colour; while (x < x1) { if (d <= 0) { d += ince; x++; } else { d += incne; x++; y++; } if (x >= 0 && x < RENDERWIDE && addTo - y >= 0 && addTo - y < RENDERDEEP) buf[(addTo - y) * RENDERWIDE + x] = colour; } } } else { // OK, y is now going to be the single increment. // Ensure the line is going top to bottom if (y1 < y0) { SWAP(x0, x1); SWAP(y0, y1); } dx = x1 - x0; dy = y1 - y0; if (dx > 0) { d = 2 * dx - dy; ince = 2 * dx; incne = 2 * (dx - dy); x = x0; y = y0; if (x >= 0 && x < RENDERWIDE && y >= 0 && y < RENDERDEEP) buf[y * RENDERWIDE + x] = colour; while (y < y1) { if (d <= 0) { d += ince; y++; } else { d += incne; x++; y++; } if (x >= 0 && x < RENDERWIDE && y >= 0 && y < RENDERDEEP) buf[y * RENDERWIDE + x] = colour; } } else { addTo = x0; x0 = 0; x1 -= addTo; x1 = -x1; dx = x1 - x0; d = 2 * dx - dy; ince = 2 * dx; incne = 2 * (dx - dy); x = x0; y = y0; if (addTo - x >= 0 && addTo - x < RENDERWIDE && y >= 0 && y < RENDERDEEP) buf[y * RENDERWIDE + addTo - x] = colour; while (y < y1) { if (d <= 0) { d += ince; y++; } else { d += incne; x++; y++; } if (addTo - x >= 0 && addTo - x < RENDERWIDE && y >= 0 && y < RENDERDEEP) buf[y * RENDERWIDE + addTo - x] = colour; } } } } /** * This function tells the driver the size of the background screen for the * current location. * @param w width of the current location * @param h height of the current location */ void Graphics::setLocationMetrics(uint16 w, uint16 h) { _locationWide = w; _locationDeep = h; setNeedFullRedraw(); } /** * Draws a parallax layer at the current position determined by the scroll. A * parallax can be either foreground, background or the main screen. */ void Graphics::renderParallax(Parallax *p, int16 l) { int16 x, y; Common::Rect r; if (_locationWide == _screenWide) x = 0; else x = ((int32) ((p->w - _screenWide) * _scrollX) / (int32) (_locationWide - _screenWide)); if (_locationDeep == _screenDeep - MENUDEEP * 2) y = 0; else y = ((int32) ((p->h - (_screenDeep - MENUDEEP * 2)) * _scrollY) / (int32) (_locationDeep - (_screenDeep - MENUDEEP * 2))); Common::Rect clipRect; // Leave enough space for the top and bottom menues clipRect.left = 0; clipRect.right = _screenWide; clipRect.top = MENUDEEP; clipRect.bottom = _screenDeep - MENUDEEP; for (int j = 0; j < _yBlocks[l]; j++) { for (int i = 0; i < _xBlocks[l]; i++) { if (_blockSurfaces[l][i + j * _xBlocks[l]]) { r.left = i * BLOCKWIDTH - x; r.right = r.left + BLOCKWIDTH; r.top = j * BLOCKHEIGHT - y + 40; r.bottom = r.top + BLOCKHEIGHT; blitBlockSurface(_blockSurfaces[l][i + j * _xBlocks[l]], &r, &clipRect); } } } _parallaxScrollX = _scrollX - x; _parallaxScrollY = _scrollY - y; } // Uncomment this when benchmarking the drawing routines. #define LIMIT_FRAME_RATE /** * Initialises the timers before the render loop is entered. */ void Graphics::initialiseRenderCycle(void) { _initialTime = _vm->_system->get_msecs(); _totalTime = _initialTime + MILLISECSPERCYCLE; } /** * This function should be called when the game engine is ready to start the * render cycle. */ void Graphics::startRenderCycle(void) { _scrollXOld = _scrollX; _scrollYOld = _scrollY; _startTime = _vm->_system->get_msecs(); if (_startTime + _renderAverageTime >= _totalTime) { _scrollX = _scrollXTarget; _scrollY = _scrollYTarget; _renderTooSlow = true; } else { _scrollX = (int16) (_scrollXOld + ((_scrollXTarget - _scrollXOld) * (_startTime - _initialTime + _renderAverageTime)) / (_totalTime - _initialTime)); _scrollY = (int16) (_scrollYOld + ((_scrollYTarget - _scrollYOld) * (_startTime - _initialTime + _renderAverageTime)) / (_totalTime - _initialTime)); _renderTooSlow = false; } if (_scrollXOld != _scrollX || _scrollYOld != _scrollY) setNeedFullRedraw(); _framesPerGameCycle = 0; } /** * This function should be called at the end of the render cycle. * @return true if the render cycle is to be terminated, * or false if it should continue */ bool Graphics::endRenderCycle(void) { static int32 renderTimeLog[4] = { 60, 60, 60, 60 }; static int32 renderCountIndex = 0; int32 time; time = _vm->_system->get_msecs(); renderTimeLog[renderCountIndex] = time - _startTime; _startTime = time; _renderAverageTime = (renderTimeLog[0] + renderTimeLog[1] + renderTimeLog[2] + renderTimeLog[3]) >> 2; _framesPerGameCycle++; if (++renderCountIndex == RENDERAVERAGETOTAL) renderCountIndex = 0; if (_renderTooSlow) { initialiseRenderCycle(); return true; } if (_startTime + _renderAverageTime >= _totalTime) { _totalTime += MILLISECSPERCYCLE; _initialTime = time; return true; } #ifdef LIMIT_FRAME_RATE if (_scrollXTarget == _scrollX && _scrollYTarget == _scrollY) { // If we have already reached the scroll target sleep for the // rest of the render cycle. _vm->sleepUntil(_totalTime); _initialTime = _vm->_system->get_msecs(); _totalTime += MILLISECSPERCYCLE; return true; } #endif // This is an attempt to ensure that we always reach the scroll target. // Otherwise the game frequently tries to pump out new interpolation // frames without ever getting anywhere. if (ABS(_scrollX - _scrollXTarget) <= 1 && ABS(_scrollY - _scrollYTarget) <= 1) { _scrollX = _scrollXTarget; _scrollY = _scrollYTarget; } else { _scrollX = (int16) (_scrollXOld + ((_scrollXTarget - _scrollXOld) * (_startTime - _initialTime + _renderAverageTime)) / (_totalTime - _initialTime)); _scrollY = (int16) (_scrollYOld + ((_scrollYTarget - _scrollYOld) * (_startTime - _initialTime + _renderAverageTime)) / (_totalTime - _initialTime)); } if (_scrollX != _scrollXOld || _scrollY != _scrollYOld) setNeedFullRedraw(); #ifdef LIMIT_FRAME_RATE // Give the other threads some breathing space. This apparently helps // against bug #875683, though I was never able to reproduce it for // myself. _vm->_system->delay_msecs(10); #endif return false; } /** * Reset scrolling stuff. This function is called from initBackground() */ void Graphics::resetRenderEngine(void) { _parallaxScrollX = 0; _parallaxScrollY = 0; _scrollX = 0; _scrollY = 0; } /** * Sets the scroll target position for the end of the game cycle. The driver * will then automatically scroll as many times as it can to reach this * position in the allotted time. */ void Graphics::setScrollTarget(int16 sx, int16 sy) { _scrollXTarget = sx; _scrollYTarget = sy; } /** * This function should be called five times with either the parallax layer * or a NULL pointer in order of background parallax to foreground parallax. */ int32 Graphics::initialiseBackgroundLayer(Parallax *p) { byte *memchunk; uint8 zeros; uint16 count; uint16 i, j, k; uint16 x; byte *data; byte *dst; ParallaxLine line; byte *pLine; debug(2, "initialiseBackgroundLayer"); // This function is called to re-initialise the layers if they have // been lost. We know this if the layers have already been assigned. if (_layer == MAXLAYERS) closeBackgroundLayer(); if (!p) { _layer++; return RD_OK; } _xBlocks[_layer] = (p->w + BLOCKWIDTH - 1) >> BLOCKWBITS; _yBlocks[_layer] = (p->h + BLOCKHEIGHT - 1) >> BLOCKHBITS; _blockSurfaces[_layer] = (BlockSurface **) calloc(_xBlocks[_layer] * _yBlocks[_layer], sizeof(BlockSurface *)); if (!_blockSurfaces[_layer]) return RDERR_OUTOFMEMORY; // Decode the parallax layer into a large chunk of memory memchunk = (byte *) calloc(_xBlocks[_layer] * _yBlocks[_layer], BLOCKWIDTH * BLOCKHEIGHT); if (!memchunk) return RDERR_OUTOFMEMORY; for (i = 0; i < p->h; i++) { if (p->offset[i] == 0) continue; pLine = (byte *) p + FROM_LE_32(p->offset[i]); line.packets = READ_LE_UINT16(pLine); line.offset = READ_LE_UINT16(pLine + 2); data = pLine + sizeof(ParallaxLine); x = line.offset; dst = memchunk + i * p->w + x; zeros = 0; if (line.packets == 0) { memcpy(dst, data, p->w); continue; } for (j = 0; j < line.packets; j++) { if (zeros) { dst += *data; x += *data; data++; zeros = 0; } else if (*data == 0) { data++; zeros = 1; } else { count = *data++; memcpy(dst, data, count); data += count; dst += count; x += count; zeros = 1; } } } // Now create the surfaces! for (i = 0; i < _xBlocks[_layer] * _yBlocks[_layer]; i++) { bool block_has_data = false; bool block_is_transparent = false; data = memchunk + (p->w * BLOCKHEIGHT * (i / _xBlocks[_layer])) + BLOCKWIDTH * (i % _xBlocks[_layer]); // FIXME: The 'block_is_transparent' flag should only consider // data that is inside the parallax layer. Still, it won't do // any harm to leave it this way... for (j = 0; j < BLOCKHEIGHT; j++) { for (k = 0; k < BLOCKWIDTH; k++) { if (data[j * p->w + k]) block_has_data = true; else block_is_transparent = true; } } // Only assign a surface to the block if it contains data. if (block_has_data) { _blockSurfaces[_layer][i] = (BlockSurface *) malloc(sizeof(BlockSurface)); // Copy the data into the surfaces. dst = _blockSurfaces[_layer][i]->data; for (j = 0; j < BLOCKHEIGHT; j++) { memcpy(dst, data, BLOCKWIDTH); data += p->w; dst += BLOCKWIDTH; } _blockSurfaces[_layer][i]->transparent = block_is_transparent; } else _blockSurfaces[_layer][i] = NULL; } free(memchunk); _layer++; return RD_OK; } /** * Should be called once after leaving the room to free up memory. */ void Graphics::closeBackgroundLayer(void) { debug(2, "CloseBackgroundLayer"); for (int j = 0; j < MAXLAYERS; j++) { if (_blockSurfaces[j]) { for (int i = 0; i < _xBlocks[j] * _yBlocks[j]; i++) if (_blockSurfaces[j][i]) free(_blockSurfaces[j][i]); free(_blockSurfaces[j]); _blockSurfaces[j] = NULL; } } _layer = 0; } #ifdef BACKEND_8BIT void Graphics::plotYUV(byte *lut, int width, int height, byte *const *dat) { byte *buf = _buffer + ((480 - height) / 2) * RENDERWIDE + (640 - width) / 2; int x, y; int ypos = 0; int cpos = 0; int linepos = 0; for (y = 0; y < height; y += 2) { for (x = 0; x < width; x += 2) { int i = ((((dat[2][cpos] + ROUNDADD) >> SHIFT) * (BITDEPTH+1)) + ((dat[1][cpos] + ROUNDADD)>>SHIFT)) * (BITDEPTH+1); cpos++; buf[linepos ] = lut[i + ((dat[0][ ypos ] + ROUNDADD) >> SHIFT)]; buf[RENDERWIDE + linepos++] = lut[i + ((dat[0][width + ypos++] + ROUNDADD) >> SHIFT)]; buf[linepos ] = lut[i + ((dat[0][ ypos ] + ROUNDADD) >> SHIFT)]; buf[RENDERWIDE + linepos++] = lut[i + ((dat[0][width + ypos++] + ROUNDADD) >> SHIFT)]; } linepos += (2 * RENDERWIDE - width); ypos += width; } } #endif } // End of namespace Sword2