//
// Copyright(C) 1993-1996 Id Software, Inc.
// Copyright(C) 2005-2014 Simon Howard
//
// 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.
//
// DESCRIPTION:
//     Screen scale-up code: 
//         1x,2x,3x,4x pixel doubling
//         Aspect ratio-correcting stretch functions
//

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "doomtype.h"

#include "i_video.h"
#include "m_argv.h"
#include "z_zone.h"

// Should be I_VideoBuffer

static byte *src_buffer;

// Destination buffer, ie. screen->pixels.

static byte *dest_buffer;

// Pitch of destination buffer, ie. screen->pitch.

static int dest_pitch;

// Lookup tables used for aspect ratio correction stretching code.
// stretch_tables[0] : 20% / 80%
// stretch_tables[1] : 40% / 60%
// All other combinations can be reached from these two tables.

static byte *stretch_tables[2] = { NULL, NULL };

// 50%/50% stretch table, for 800x600 squash mode

static byte *half_stretch_table = NULL;

// Called to set the source and destination buffers before doing the
// scale.

void I_InitScale(byte *_src_buffer, byte *_dest_buffer, int _dest_pitch)
{
    src_buffer = _src_buffer;
    dest_buffer = _dest_buffer;
    dest_pitch = _dest_pitch;
}

//
// Pixel doubling scale-up functions.
//

// 1x scale doesn't really do any scaling: it just copies the buffer
// a line at a time for when pitch != SCREENWIDTH (!native_surface)

static boolean I_Scale1x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp;
    int y;
    int w = x2 - x1;
    
    // Need to byte-copy from buffer into the screen buffer

    bufp = src_buffer + y1 * SCREENWIDTH + x1;
    screenp = (byte *) dest_buffer + y1 * dest_pitch + x1;

    for (y=y1; y<y2; ++y)
    {
        memcpy(screenp, bufp, w);
        screenp += dest_pitch;
        bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_scale_1x = {
    SCREENWIDTH, SCREENHEIGHT,
    NULL,
    I_Scale1x,
    false,
};

// 2x scale (640x400)

static boolean I_Scale2x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp, *screenp2;
    int x, y;
    int multi_pitch;

    multi_pitch = dest_pitch * 2;
    bufp = src_buffer + y1 * SCREENWIDTH + x1;
    screenp = (byte *) dest_buffer + (y1 * dest_pitch + x1) * 2;
    screenp2 = screenp + dest_pitch;

    for (y=y1; y<y2; ++y)
    {
        byte *sp, *sp2, *bp;
        sp = screenp;
        sp2 = screenp2;
        bp = bufp;

        for (x=x1; x<x2; ++x)
        {
            *sp++ = *bp;  *sp++ = *bp;
            *sp2++ = *bp; *sp2++ = *bp;
            ++bp;
        }
        screenp += multi_pitch;
        screenp2 += multi_pitch;
        bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_scale_2x = {
    SCREENWIDTH * 2, SCREENHEIGHT * 2,
    NULL,
    I_Scale2x,
    false,
};

// 3x scale (960x600)

static boolean I_Scale3x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp, *screenp2, *screenp3;
    int x, y;
    int multi_pitch;

    multi_pitch = dest_pitch * 3;
    bufp = src_buffer + y1 * SCREENWIDTH + x1;
    screenp = (byte *) dest_buffer + (y1 * dest_pitch + x1) * 3;
    screenp2 = screenp + dest_pitch;
    screenp3 = screenp + dest_pitch * 2;

    for (y=y1; y<y2; ++y)
    {
        byte *sp, *sp2, *sp3, *bp;
        sp = screenp;
        sp2 = screenp2;
        sp3 = screenp3;
        bp = bufp;

        for (x=x1; x<x2; ++x)
        {
            *sp++ = *bp;  *sp++ = *bp;  *sp++ = *bp;
            *sp2++ = *bp; *sp2++ = *bp; *sp2++ = *bp;
            *sp3++ = *bp; *sp3++ = *bp; *sp3++ = *bp;
            ++bp;
        }
        screenp += multi_pitch;
        screenp2 += multi_pitch;
        screenp3 += multi_pitch;
        bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_scale_3x = {
    SCREENWIDTH * 3, SCREENHEIGHT * 3,
    NULL,
    I_Scale3x,
    false,
};

// 4x scale (1280x800)

static boolean I_Scale4x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp, *screenp2, *screenp3, *screenp4;
    int x, y;
    int multi_pitch;

    multi_pitch = dest_pitch * 4;
    bufp = src_buffer + y1 * SCREENWIDTH + x1;
    screenp = (byte *) dest_buffer + (y1 * dest_pitch + x1) * 4;
    screenp2 = screenp + dest_pitch;
    screenp3 = screenp + dest_pitch * 2;
    screenp4 = screenp + dest_pitch * 3;

    for (y=y1; y<y2; ++y)
    {
        byte *sp, *sp2, *sp3, *sp4, *bp;
        sp = screenp;
        sp2 = screenp2;
        sp3 = screenp3;
        sp4 = screenp4;
        bp = bufp;

        for (x=x1; x<x2; ++x)
        {
            *sp++ = *bp;  *sp++ = *bp;  *sp++ = *bp;  *sp++ = *bp;
            *sp2++ = *bp; *sp2++ = *bp; *sp2++ = *bp; *sp2++ = *bp;
            *sp3++ = *bp; *sp3++ = *bp; *sp3++ = *bp; *sp3++ = *bp;
            *sp4++ = *bp; *sp4++ = *bp; *sp4++ = *bp; *sp4++ = *bp;
            ++bp;
        }
        screenp += multi_pitch;
        screenp2 += multi_pitch;
        screenp3 += multi_pitch;
        screenp4 += multi_pitch;
        bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_scale_4x = {
    SCREENWIDTH * 4, SCREENHEIGHT * 4,
    NULL,
    I_Scale4x,
    false,
};

// 5x scale (1600x1000)

static boolean I_Scale5x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp, *screenp2, *screenp3, *screenp4, *screenp5;
    int x, y;
    int multi_pitch;

    multi_pitch = dest_pitch * 5;
    bufp = src_buffer + y1 * SCREENWIDTH + x1;
    screenp = (byte *) dest_buffer + (y1 * dest_pitch + x1) * 5;
    screenp2 = screenp + dest_pitch;
    screenp3 = screenp + dest_pitch * 2;
    screenp4 = screenp + dest_pitch * 3;
    screenp5 = screenp + dest_pitch * 4;

    for (y=y1; y<y2; ++y)
    {
        byte *sp, *sp2, *sp3, *sp4, *sp5, *bp;
        sp = screenp;
        sp2 = screenp2;
        sp3 = screenp3;
        sp4 = screenp4;
        sp5 = screenp5;
        bp = bufp;

        for (x=x1; x<x2; ++x)
        {
            *sp++ = *bp;  *sp++ = *bp;  *sp++ = *bp;  *sp++ = *bp;  *sp++ = *bp;
            *sp2++ = *bp; *sp2++ = *bp; *sp2++ = *bp; *sp2++ = *bp; *sp2++ = *bp;
            *sp3++ = *bp; *sp3++ = *bp; *sp3++ = *bp; *sp3++ = *bp; *sp3++ = *bp;
            *sp4++ = *bp; *sp4++ = *bp; *sp4++ = *bp; *sp4++ = *bp; *sp4++ = *bp;
            *sp5++ = *bp; *sp5++ = *bp; *sp5++ = *bp; *sp5++ = *bp; *sp5++ = *bp;
            ++bp;
        }
        screenp += multi_pitch;
        screenp2 += multi_pitch;
        screenp3 += multi_pitch;
        screenp4 += multi_pitch;
        screenp5 += multi_pitch;
        bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_scale_5x = {
    SCREENWIDTH * 5, SCREENHEIGHT * 5,
    NULL,
    I_Scale5x,
    false,
};


// Search through the given palette, finding the nearest color that matches
// the given color.

static int FindNearestColor(byte *palette, int r, int g, int b)
{
    byte *col;
    int best;
    int best_diff;
    int diff;
    int i;

    best = 0;
    best_diff = INT_MAX;

    for (i=0; i<256; ++i)
    {
        col = palette + i * 3;
        diff = (r - col[0]) * (r - col[0])
             + (g - col[1]) * (g - col[1])
             + (b - col[2]) * (b - col[2]);

        if (diff == 0)
        {
            return i;
        }
        else if (diff < best_diff)
        {
            best = i;
            best_diff = diff;
        }
    }

    return best;
}

// Create a stretch table.  This is a lookup table for blending colors.
// pct specifies the bias between the two colors: 0 = all y, 100 = all x.
// NB: This is identical to the lookup tables used in other ports for
// translucency.

static byte *GenerateStretchTable(byte *palette, int pct)
{
    byte *result;
    int x, y;
    int r, g, b;
    byte *col1;
    byte *col2;

    result = Z_Malloc(256 * 256, PU_STATIC, NULL);

    for (x=0; x<256; ++x)
    {
        for (y=0; y<256; ++y)
        {
            col1 = palette + x * 3;
            col2 = palette + y * 3;
            r = (((int) col1[0]) * pct + ((int) col2[0]) * (100 - pct)) / 100;
            g = (((int) col1[1]) * pct + ((int) col2[1]) * (100 - pct)) / 100;
            b = (((int) col1[2]) * pct + ((int) col2[2]) * (100 - pct)) / 100;
            result[x * 256 + y] = FindNearestColor(palette, r, g, b);
        }
    }

    return result;
}

// Called at startup to generate the lookup tables for aspect ratio
// correcting scale up.

static void I_InitStretchTables(byte *palette)
{
    if (stretch_tables[0] != NULL)
    {
        return;
    }

    // We only actually need two lookup tables:
    //
    // mix 0%   =  just write line 1
    // mix 20%  =  stretch_tables[0]
    // mix 40%  =  stretch_tables[1]
    // mix 60%  =  stretch_tables[1] used backwards
    // mix 80%  =  stretch_tables[0] used backwards
    // mix 100% =  just write line 2

    printf("I_InitStretchTables: Generating lookup tables..");
    fflush(stdout);
    stretch_tables[0] = GenerateStretchTable(palette, 20);
    printf(".."); fflush(stdout);
    stretch_tables[1] = GenerateStretchTable(palette, 40);
    puts("");
}

// Create 50%/50% table for 800x600 squash mode

static void I_InitSquashTable(byte *palette)
{
    if (half_stretch_table != NULL)
    {
        return;
    }

    printf("I_InitSquashTable: Generating lookup table..");
    fflush(stdout);
    half_stretch_table = GenerateStretchTable(palette, 50);
    puts("");
}

// Destroy the scaling lookup tables. This should only ever be called
// if switching to a completely different palette from the normal one
// (in which case the mappings no longer make any sense).

void I_ResetScaleTables(byte *palette)
{
    if (stretch_tables[0] != NULL)
    {
        Z_Free(stretch_tables[0]);
        Z_Free(stretch_tables[1]);

        printf("I_ResetScaleTables: Regenerating lookup tables..\n");
        stretch_tables[0] = GenerateStretchTable(palette, 20);
        stretch_tables[1] = GenerateStretchTable(palette, 40);
    }

    if (half_stretch_table != NULL)
    {
        Z_Free(half_stretch_table);

        printf("I_ResetScaleTables: Regenerating lookup table..\n");

        half_stretch_table = GenerateStretchTable(palette, 50);
    }
}


// 
// Aspect ratio correcting scale up functions.
//
// These double up pixels to stretch the screen when using a 4:3
// screen mode.
//

static inline void WriteBlendedLine1x(byte *dest, byte *src1, byte *src2, 
                               byte *stretch_table)
{
    int x;

    for (x=0; x<SCREENWIDTH; ++x)
    {
        *dest = stretch_table[*src1 * 256 + *src2];
        ++dest;
        ++src1;
        ++src2;
    }
} 

// 1x stretch (320x240)

static boolean I_Stretch1x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp;
    int y;

    // Only works with full screen update

    if (x1 != 0 || y1 != 0 || x2 != SCREENWIDTH || y2 != SCREENHEIGHT)
    {
        return false;
    }    

    // Need to byte-copy from buffer into the screen buffer

    bufp = src_buffer + y1 * SCREENWIDTH + x1;
    screenp = (byte *) dest_buffer + y1 * dest_pitch + x1;

    // For every 5 lines of src_buffer, 6 lines are written to dest_buffer
    // (200 -> 240)

    for (y=0; y<SCREENHEIGHT; y += 5)
    {
        // 100% line 0
        memcpy(screenp, bufp, SCREENWIDTH);
        screenp += dest_pitch;

        // 20% line 0, 80% line 1
        WriteBlendedLine1x(screenp, bufp, bufp + SCREENWIDTH, stretch_tables[0]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 40% line 1, 60% line 2
        WriteBlendedLine1x(screenp, bufp, bufp + SCREENWIDTH, stretch_tables[1]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 60% line 2, 40% line 3
        WriteBlendedLine1x(screenp, bufp + SCREENWIDTH, bufp, stretch_tables[1]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 80% line 3, 20% line 4
        WriteBlendedLine1x(screenp, bufp + SCREENWIDTH, bufp, stretch_tables[0]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 4
        memcpy(screenp, bufp, SCREENWIDTH);
        screenp += dest_pitch; bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_stretch_1x = {
    SCREENWIDTH, SCREENHEIGHT_4_3,
    I_InitStretchTables,
    I_Stretch1x,
    true,
};

static inline void WriteLine2x(byte *dest, byte *src)
{
    int x;

    for (x=0; x<SCREENWIDTH; ++x)
    {
        dest[0] = *src;
        dest[1] = *src;
        dest += 2;
        ++src;
    }
}

static inline void WriteBlendedLine2x(byte *dest, byte *src1, byte *src2, 
                               byte *stretch_table)
{
    int x;
    int val;

    for (x=0; x<SCREENWIDTH; ++x)
    {
        val = stretch_table[*src1 * 256 + *src2];
        dest[0] = val;
        dest[1] = val;
        dest += 2;
        ++src1;
        ++src2;
    }
} 

// 2x stretch (640x480)

static boolean I_Stretch2x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp;
    int y;

    // Only works with full screen update

    if (x1 != 0 || y1 != 0 || x2 != SCREENWIDTH || y2 != SCREENHEIGHT)
    {
        return false;
    }    

    // Need to byte-copy from buffer into the screen buffer

    bufp = src_buffer + y1 * SCREENWIDTH + x1;
    screenp = (byte *) dest_buffer + y1 * dest_pitch + x1;

    // For every 5 lines of src_buffer, 12 lines are written to dest_buffer.
    // (200 -> 480)

    for (y=0; y<SCREENHEIGHT; y += 5)
    {
        // 100% line 0
        WriteLine2x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine2x(screenp, bufp);
        screenp += dest_pitch;

        // 40% line 0, 60% line 1
        WriteBlendedLine2x(screenp, bufp, bufp + SCREENWIDTH, stretch_tables[1]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 1
        WriteLine2x(screenp, bufp);
        screenp += dest_pitch;

        // 80% line 1, 20% line 2
        WriteBlendedLine2x(screenp, bufp + SCREENWIDTH, bufp, stretch_tables[0]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 2
        WriteLine2x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 2
        WriteLine2x(screenp, bufp);
        screenp += dest_pitch;

        // 20% line 2, 80% line 3
        WriteBlendedLine2x(screenp, bufp, bufp + SCREENWIDTH, stretch_tables[0]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 3
        WriteLine2x(screenp, bufp);
        screenp += dest_pitch;

        // 60% line 3, 40% line 4
        WriteBlendedLine2x(screenp, bufp + SCREENWIDTH, bufp, stretch_tables[1]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 4
        WriteLine2x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 4
        WriteLine2x(screenp, bufp);
        screenp += dest_pitch; bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_stretch_2x = {
    SCREENWIDTH * 2, SCREENHEIGHT_4_3 * 2,
    I_InitStretchTables,
    I_Stretch2x,
    false,
};

static inline void WriteLine3x(byte *dest, byte *src)
{
    int x;

    for (x=0; x<SCREENWIDTH; ++x)
    {
        dest[0] = *src;
        dest[1] = *src;
        dest[2] = *src;
        dest += 3;
        ++src;
    }
}

static inline void WriteBlendedLine3x(byte *dest, byte *src1, byte *src2, 
                               byte *stretch_table)
{
    int x;
    int val;

    for (x=0; x<SCREENWIDTH; ++x)
    {
        val = stretch_table[*src1 * 256 + *src2];
        dest[0] = val;
        dest[1] = val;
        dest[2] = val;
        dest += 3;
        ++src1;
        ++src2;
    }
} 

// 3x stretch (960x720)

static boolean I_Stretch3x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp;
    int y;

    // Only works with full screen update

    if (x1 != 0 || y1 != 0 || x2 != SCREENWIDTH || y2 != SCREENHEIGHT)
    {
        return false;
    }    

    // Need to byte-copy from buffer into the screen buffer

    bufp = src_buffer + y1 * SCREENWIDTH + x1;
    screenp = (byte *) dest_buffer + y1 * dest_pitch + x1;

    // For every 5 lines of src_buffer, 18 lines are written to dest_buffer.
    // (200 -> 720)

    for (y=0; y<SCREENHEIGHT; y += 5)
    {
        // 100% line 0
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 60% line 0, 40% line 1
        WriteBlendedLine3x(screenp, bufp + SCREENWIDTH, bufp, stretch_tables[1]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 1
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 1
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 1
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 20% line 1, 80% line 2
        WriteBlendedLine3x(screenp, bufp, bufp + SCREENWIDTH, stretch_tables[0]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 2
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 2
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 80% line 2, 20% line 3
        WriteBlendedLine3x(screenp, bufp + SCREENWIDTH, bufp, stretch_tables[0]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 3
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 3
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 3
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 40% line 3, 60% line 4
        WriteBlendedLine3x(screenp, bufp, bufp + SCREENWIDTH, stretch_tables[1]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 4
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 4
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 4
        WriteLine3x(screenp, bufp);
        screenp += dest_pitch; bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_stretch_3x = {
    SCREENWIDTH * 3, SCREENHEIGHT_4_3 * 3,
    I_InitStretchTables,
    I_Stretch3x,
    false,
};

static inline void WriteLine4x(byte *dest, byte *src)
{
    int x;

    for (x=0; x<SCREENWIDTH; ++x)
    {
        dest[0] = *src;
        dest[1] = *src;
        dest[2] = *src;
        dest[3] = *src;
        dest += 4;
        ++src;
    }
}

static inline void WriteBlendedLine4x(byte *dest, byte *src1, byte *src2, 
                               byte *stretch_table)
{
    int x;
    int val;

    for (x=0; x<SCREENWIDTH; ++x)
    {
        val = stretch_table[*src1 * 256 + *src2];
        dest[0] = val;
        dest[1] = val;
        dest[2] = val;
        dest[3] = val;
        dest += 4;
        ++src1;
        ++src2;
    }
} 

// 4x stretch (1280x960)

static boolean I_Stretch4x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp;
    int y;

    // Only works with full screen update

    if (x1 != 0 || y1 != 0 || x2 != SCREENWIDTH || y2 != SCREENHEIGHT)
    {
        return false;
    }    

    // Need to byte-copy from buffer into the screen buffer

    bufp = src_buffer + y1 * SCREENWIDTH + x1;
    screenp = (byte *) dest_buffer + y1 * dest_pitch + x1;

    // For every 5 lines of src_buffer, 24 lines are written to dest_buffer.
    // (200 -> 960)

    for (y=0; y<SCREENHEIGHT; y += 5)
    {
        // 100% line 0
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 90% line 0, 20% line 1
        WriteBlendedLine4x(screenp, bufp + SCREENWIDTH, bufp, stretch_tables[0]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 1
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 1
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 1
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 1
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 60% line 1, 40% line 2
        WriteBlendedLine4x(screenp, bufp + SCREENWIDTH, bufp, stretch_tables[1]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 2
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 2
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 2
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 2
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 40% line 2, 60% line 3
        WriteBlendedLine4x(screenp, bufp, bufp + SCREENWIDTH, stretch_tables[1]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 3
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 3
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 3
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 3
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 20% line 3, 80% line 4
        WriteBlendedLine4x(screenp, bufp, bufp + SCREENWIDTH, stretch_tables[0]);
        screenp += dest_pitch; bufp += SCREENWIDTH;

        // 100% line 4
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 4
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 4
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 4
        WriteLine4x(screenp, bufp);
        screenp += dest_pitch; bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_stretch_4x = {
    SCREENWIDTH * 4, SCREENHEIGHT_4_3 * 4,
    I_InitStretchTables,
    I_Stretch4x,
    false,
};

static inline void WriteLine5x(byte *dest, byte *src)
{
    int x;

    for (x=0; x<SCREENWIDTH; ++x)
    {
        dest[0] = *src;
        dest[1] = *src;
        dest[2] = *src;
        dest[3] = *src;
        dest[4] = *src;
        dest += 5;
        ++src;
    }
}

// 5x stretch (1600x1200)

static boolean I_Stretch5x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp;
    int y;

    // Only works with full screen update

    if (x1 != 0 || y1 != 0 || x2 != SCREENWIDTH || y2 != SCREENHEIGHT)
    {
        return false;
    }    

    // Need to byte-copy from buffer into the screen buffer

    bufp = src_buffer + y1 * SCREENWIDTH + x1;
    screenp = (byte *) dest_buffer + y1 * dest_pitch + x1;

    // For every 1 line of src_buffer, 6 lines are written to dest_buffer.
    // (200 -> 1200)

    for (y=0; y<SCREENHEIGHT; y += 1)
    {
        // 100% line 0
        WriteLine5x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine5x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine5x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine5x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine5x(screenp, bufp);
        screenp += dest_pitch;

        // 100% line 0
        WriteLine5x(screenp, bufp);
        screenp += dest_pitch; bufp += SCREENWIDTH;
    }

    // test hack for Porsche Monty... scan line simulation:
    // See here: https://www.doomworld.com/vb/post/962612

    if (M_CheckParm("-scanline") > 0)
    {
        screenp = (byte *) dest_buffer + 2 * dest_pitch;

        for (y=0; y<1198; y += 3)
        {
            memset(screenp, 0, 1600);

            screenp += dest_pitch * 3;
        }
    }

    return true;
}

screen_mode_t mode_stretch_5x = {
    SCREENWIDTH * 5, SCREENHEIGHT_4_3 * 5,
    I_InitStretchTables,
    I_Stretch5x,
    false,
};

//
// Aspect ratio correcting "squash" functions. 
//
// These do the opposite of the "stretch" functions above: while the
// stretch functions increase the vertical dimensions, the squash
// functions decrease the horizontal dimensions for the same result.
//
// The same blend tables from the stretch functions are reused; as 
// a result, the dimensions are *slightly* wrong (eg. 320x200 should
// squash to 266x200, but actually squashes to 256x200).
//

// 
// 1x squashed scale (256x200)
//

static inline void WriteSquashedLine1x(byte *dest, byte *src)
{
    int x;

    for (x=0; x<SCREENWIDTH; )
    {
        // Draw in blocks of 5

        // 80% pixel 0,   20% pixel 1

        *dest++ = stretch_tables[0][src[1] * 256 + src[0]];

        // 60% pixel 1,   40% pixel 2

        *dest++ = stretch_tables[1][src[2] * 256 + src[1]];

        // 40% pixel 2,   60% pixel 3

        *dest++ = stretch_tables[1][src[2] * 256 + src[3]];

        // 20% pixel 3,   80% pixel 4

        *dest++ = stretch_tables[0][src[3] * 256 + src[4]];

        x += 5;
        src += 5;
    }
}

// 1x squashed (256x200)

static boolean I_Squash1x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp;
    int y;

    // Only works with full screen update

    if (x1 != 0 || y1 != 0 || x2 != SCREENWIDTH || y2 != SCREENHEIGHT)
    {
        return false;
    }    

    bufp = src_buffer;
    screenp = (byte *) dest_buffer;

    for (y=0; y<SCREENHEIGHT; ++y) 
    {
        WriteSquashedLine1x(screenp, bufp);

        screenp += dest_pitch;
        bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_squash_1x = {
    SCREENWIDTH_4_3, SCREENHEIGHT,
    I_InitStretchTables,
    I_Squash1x,
    true,
};


//
// 2x squashed scale (512x400)
//

#define DRAW_PIXEL2 \
      *dest++ = *dest2++ = c;

static inline void WriteSquashedLine2x(byte *dest, byte *src)
{
    byte *dest2;
    int x, c;

    dest2 = dest + dest_pitch;

    for (x=0; x<SCREENWIDTH; )
    {
        // Draw in blocks of 5

        // 100% pixel 0

        c = src[0];
        DRAW_PIXEL2;

        // 60% pixel 0, 40% pixel 1

        c = stretch_tables[1][src[1] * 256 + src[0]];
        DRAW_PIXEL2;

        // 100% pixel 1

        c = src[1];
        DRAW_PIXEL2;

        // 20% pixel 1, 80% pixel 2

        c = stretch_tables[0][src[1] * 256 + src[2]];
        DRAW_PIXEL2;

        // 80% pixel 2, 20% pixel 3

        c = stretch_tables[0][src[3] * 256 + src[2]];
        DRAW_PIXEL2;

        // 100% pixel 3

        c = src[3];
        DRAW_PIXEL2;

        // 40% pixel 3, 60% pixel 4

        c = stretch_tables[1][src[3] * 256 + src[4]];
        DRAW_PIXEL2;

        // 100% pixel 4

        c = src[4];
        DRAW_PIXEL2;

        x += 5;
        src += 5;
    }
}

// 2x squash (512x400)

static boolean I_Squash2x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp;
    int y;

    // Only works with full screen update

    if (x1 != 0 || y1 != 0 || x2 != SCREENWIDTH || y2 != SCREENHEIGHT)
    {
        return false;
    }    

    bufp = src_buffer;
    screenp = (byte *) dest_buffer;

    for (y=0; y<SCREENHEIGHT; ++y) 
    {
        WriteSquashedLine2x(screenp, bufp);

        screenp += dest_pitch * 2;
        bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_squash_2x = {
    SCREENWIDTH_4_3 * 2, SCREENHEIGHT * 2,
    I_InitStretchTables,
    I_Squash2x,
    false,
};


#define DRAW_PIXEL3 \
        *dest++ = *dest2++ = *dest3++ = c

static inline void WriteSquashedLine3x(byte *dest, byte *src)
{
    byte *dest2, *dest3;
    int x, c;

    dest2 = dest + dest_pitch;
    dest3 = dest + dest_pitch * 2;

    for (x=0; x<SCREENWIDTH; )
    {
        // Every 2 pixels is expanded to 5 pixels

        // 100% pixel 0 x2

        c = src[0];

        DRAW_PIXEL3;
        DRAW_PIXEL3;

        // 50% pixel 0, 50% pixel 1

        c = half_stretch_table[src[0] * 256 + src[1]];

        DRAW_PIXEL3;

        // 100% pixel 1

        c = src[1];

        DRAW_PIXEL3;
        DRAW_PIXEL3;

        x += 2;
        src += 2;
    }
}


//
// 3x scale squashed (800x600)
//
// This is a special case that uses the half_stretch_table (50%) rather
// than the normal stretch_tables(20,40%), to scale up to 800x600 
// exactly.
//

static boolean I_Squash3x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp;
    int y;

    // Only works with full screen update

    if (x1 != 0 || y1 != 0 || x2 != SCREENWIDTH || y2 != SCREENHEIGHT)
    {
        return false;
    }    

    bufp = src_buffer;
    screenp = (byte *) dest_buffer;

    for (y=0; y<SCREENHEIGHT; ++y) 
    {
        WriteSquashedLine3x(screenp, bufp);

        screenp += dest_pitch * 3;
        bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_squash_3x = {
    800, 600,
    I_InitSquashTable,
    I_Squash3x,
    false,
};

#define DRAW_PIXEL4 \
        *dest++ = *dest2++ = *dest3++ = *dest4++ = c;
      
static inline void WriteSquashedLine4x(byte *dest, byte *src)
{
    int x;
    int c;
    byte *dest2, *dest3, *dest4;

    dest2 = dest + dest_pitch;
    dest3 = dest + dest_pitch * 2;
    dest4 = dest + dest_pitch * 3;

    for (x=0; x<SCREENWIDTH; )
    {
        // Draw in blocks of 5

        // 100% pixel 0  x3

        c = src[0];
        DRAW_PIXEL4;
        DRAW_PIXEL4;
        DRAW_PIXEL4;

        // 20% pixel 0,  80% pixel 1

        c = stretch_tables[0][src[0] * 256 + src[1]];
        DRAW_PIXEL4;

        // 100% pixel 1 x2

        c = src[1];
        DRAW_PIXEL4;
        DRAW_PIXEL4;

        // 40% pixel 1, 60% pixel 2

        c = stretch_tables[1][src[1] * 256 + src[2]];
        DRAW_PIXEL4;

        // 100% pixel 2 x2

        c = src[2];
        DRAW_PIXEL4;
        DRAW_PIXEL4;

        // 60% pixel 2, 40% pixel 3

        c = stretch_tables[1][src[3] * 256 + src[2]];
        DRAW_PIXEL4;

        // 100% pixel 3 x2

        c = src[3];
        DRAW_PIXEL4;
        DRAW_PIXEL4;

        // 80% pixel 3, 20% pixel 4

        c = stretch_tables[0][src[4] * 256 + src[3]];
        DRAW_PIXEL4;

        // 100% pixel 4

        c = src[4];
        DRAW_PIXEL4;
        DRAW_PIXEL4;
        DRAW_PIXEL4;

        x += 5;
        src += 5;
    }
}

//
// 4x squashed (1024x800)
//

static boolean I_Squash4x(int x1, int y1, int x2, int y2)
{
    byte *bufp, *screenp;
    int y;

    // Only works with full screen update

    if (x1 != 0 || y1 != 0 || x2 != SCREENWIDTH || y2 != SCREENHEIGHT)
    {
        return false;
    }    

    bufp = src_buffer;
    screenp = (byte *) dest_buffer;

    for (y=0; y<SCREENHEIGHT; ++y) 
    {
        WriteSquashedLine4x(screenp, bufp);

        screenp += dest_pitch * 4;
        bufp += SCREENWIDTH;
    }

    return true;
}

screen_mode_t mode_squash_4x = {
    SCREENWIDTH_4_3 * 4, SCREENHEIGHT * 4,
    I_InitStretchTables,
    I_Squash4x,
    false,
};

// We used to have mode_squash_5x here as well, but it got removed.
// 5x squashing gives 1280x1000, which is very close to the 4x stretched
// 1280x960. The difference is that 1280x1000 is the wrong aspect ratio.
// It was ultimately decided that it was better to use the right aspect
// ratio and have slightly larger borders than to have slightly smaller
// windowboxing borders. It also means that the aspect ratio is correct
// when running at 1280x1024. See bug #460 for more details, or this
// post: https://www.doomworld.com/vb/post/1316735

