// for finding memory leaks in debug mode with Visual Studio 
#if defined _DEBUG && defined _MSC_VER
#include <crtdbg.h>
#endif

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <math.h> // modf()
#ifdef _WIN32
#define WIN32_MEAN_AND_LEAN
#include <windows.h>
#include <SDL2/SDL_syswm.h>
#endif
#include <stdint.h>
#include <stdbool.h>
#ifndef _WIN32
#include <unistd.h> // usleep()
#endif
#include <ctype.h> // tolower()
#include "pt2_header.h"
#include "pt2_keyboard.h"
#include "pt2_mouse.h"
#include "pt2_audio.h"
#include "pt2_palette.h"
#include "pt2_helpers.h"
#include "pt2_textout.h"
#include "pt2_tables.h"
#include "pt2_modloader.h"
#include "pt2_sampleloader.h"
#include "pt2_patternviewer.h"
#include "pt2_sampler.h"
#include "pt2_diskop.h"
#include "pt2_visuals.h"
#include "pt2_helpers.h"
#include "pt2_scopes.h"
#include "pt2_edit.h"

typedef struct sprite_t
{
	bool visible;
	int8_t pixelType;
	int16_t newX, newY, x, y, w, h;
	uint32_t colorKey, *refreshBuffer;
	const void *data;
} sprite_t;

static uint32_t vuMetersBg[4 * (10 * 48)];
static uint64_t timeNext64, timeNext64Frac;

sprite_t sprites[SPRITE_NUM]; // globalized

static const uint16_t cursorPosTable[24] =
{
	 30,  54,  62,  70,  78,  86,
	102, 126, 134, 142, 150, 158,
	174, 198, 206, 214, 222, 230,
	246, 270, 278, 286, 294, 302
};

bool intMusic(void); // pt_modplayer.c
extern int32_t samplesPerTick; // pt_audio.c
void storeTempVariables(void); // pt_modplayer.c
void updateSongInfo1(void);
void updateSongInfo2(void);
void updateSampler(void);
void updatePatternData(void);
void updateMOD2WAVDialog(void);

void statusAllRight(void)
{
	setStatusMessage("ALL RIGHT", DO_CARRY);
}

void statusOutOfMemory(void)
{
	displayErrorMsg("OUT OF MEMORY !!!");
}

void setupPerfFreq(void)
{
	uint64_t perfFreq64;
	double dInt, dFrac;

	perfFreq64 = SDL_GetPerformanceFrequency(); assert(perfFreq64 != 0);
	editor.dPerfFreq = (double)perfFreq64;
	editor.dPerfFreqMulMicro = 1000000.0 / editor.dPerfFreq;

	// calculate vblank time for performance counters and split into int/frac
	dFrac = modf(editor.dPerfFreq / VBLANK_HZ, &dInt);

	// integer part
	editor.vblankTimeLen = (int32_t)dInt;

	// fractional part scaled to 0..2^32-1
	dFrac *= UINT32_MAX;
	editor.vblankTimeLenFrac = (uint32_t)(dFrac + 0.5);
}

void setupWaitVBL(void)
{
	// set next frame time
	timeNext64 = SDL_GetPerformanceCounter() + editor.vblankTimeLen;
	timeNext64Frac = editor.vblankTimeLenFrac;
}

void waitVBL(void)
{
	// this routine almost never delays if we have 60Hz vsync, but it's still needed in some occasions

	int32_t time32;
	uint32_t diff32;
	uint64_t time64;

	time64 = SDL_GetPerformanceCounter();
	if (time64 < timeNext64)
	{
		assert(timeNext64-time64 <= 0xFFFFFFFFULL);
		diff32 = (uint32_t)(timeNext64 - time64);

		// convert to microseconds and round to integer
		time32 = (int32_t)((diff32 * editor.dPerfFreqMulMicro) + 0.5);

		// delay until we have reached next tick
		if (time32 > 0)
			usleep(time32);
	}

	// update next tick time
	timeNext64 += editor.vblankTimeLen;
	timeNext64Frac += editor.vblankTimeLenFrac;
	if (timeNext64Frac > 0xFFFFFFFF)
	{
		timeNext64Frac &= 0xFFFFFFFF;
		timeNext64++;
	}
}

void renderFrame(void)
{
	updateMOD2WAVDialog(); // must be first to avoid flickering issues

	updateSongInfo1(); // top left side of screen, when "disk op"/"pos ed" is hidden
	updateSongInfo2(); // two middle rows of screen, always visible
	updateEditOp();
	updatePatternData();
	updateDiskOp();
	updateSampler();
	updatePosEd();
	updateVisualizer();
	handleLastGUIObjectDown();
	drawSamplerLine();
}

void resetAllScreens(void)
{
	editor.mixFlag = false;
	editor.swapChannelFlag = false;
	editor.ui.clearScreenShown = false;
	editor.ui.changingChordNote = false;
	editor.ui.changingSmpResample = false;
	editor.ui.pat2SmpDialogShown = false;
	editor.ui.disablePosEd = false;
	editor.ui.disableVisualizer = false;

	if (editor.ui.samplerScreenShown)
	{
		editor.ui.samplerVolBoxShown = false;
		editor.ui.samplerFiltersBoxShown = false;

		displaySample();
	}

	if (editor.ui.editTextFlag)
		exitGetTextLine(EDIT_TEXT_NO_UPDATE);
}

void removeAskDialog(void)
{
	if (!editor.ui.askScreenShown && !editor.isWAVRendering)
		displayMainScreen();

	editor.ui.disablePosEd = false;
	editor.ui.disableVisualizer = false;
}

void renderAskDialog(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	editor.ui.disablePosEd = true;
	editor.ui.disableVisualizer = true;

	// render ask dialog

	srcPtr = editor.ui.pat2SmpDialogShown ? pat2SmpDialogBMP : yesNoDialogBMP;
	dstPtr = &video.frameBuffer[(51 * SCREEN_W) + 160];

	for (uint32_t y = 0; y < 39; y++)
	{
		memcpy(dstPtr, srcPtr, 104 * sizeof (int32_t));

		srcPtr += 104;
		dstPtr += SCREEN_W;
	}
}

void renderBigAskDialog(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	editor.ui.disablePosEd = true;
	editor.ui.disableVisualizer = true;

	// render custom big ask dialog

	srcPtr = bigYesNoDialogBMP;
	dstPtr = &video.frameBuffer[(44 * SCREEN_W) + 120];

	for (uint32_t y = 0; y < 55; y++)
	{
		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));

		srcPtr += 200;
		dstPtr += SCREEN_W;
	}
}

void showDownsampleAskDialog(void)
{
	editor.ui.askScreenShown = true;
	editor.ui.askScreenType = ASK_LOAD_DOWNSAMPLE;
	pointerSetMode(POINTER_MODE_MSG1, NO_CARRY);
	setStatusMessage("PLEASE SELECT", NO_CARRY);
	renderBigAskDialog();

	textOutTight(133, 49, "THE SAMPLE'S FREQUENCY IS", video.palette[PAL_BACKGRD]);
	textOutTight(178, 57, "ABOVE 22KHZ.", video.palette[PAL_BACKGRD]);
	textOutTight(133, 65, "DO YOU WANT TO DOWNSAMPLE", video.palette[PAL_BACKGRD]);
	textOutTight(156, 73, "BEFORE LOADING IT?", video.palette[PAL_BACKGRD]);
}

static void fillFromVuMetersBgBuffer(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	if (editor.ui.samplerScreenShown || editor.isWAVRendering || editor.isSMPRendering)
		return;

	srcPtr = vuMetersBg;
	dstPtr = &video.frameBuffer[(187 * SCREEN_W) + 55];

	for (uint32_t i = 0; i < AMIGA_VOICES; i++)
	{
		for (uint32_t y = 0; y < 48; y++)
		{
			for (uint32_t x = 0; x < 10; x++)
				dstPtr[x] = srcPtr[x];

			srcPtr += 10;
			dstPtr -= SCREEN_W;
		}

		dstPtr += (SCREEN_W * 48) + 72;
	}
}

void fillToVuMetersBgBuffer(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	if (editor.ui.samplerScreenShown || editor.isWAVRendering || editor.isSMPRendering)
		return;

	srcPtr = &video.frameBuffer[(187 * SCREEN_W) + 55];
	dstPtr = vuMetersBg;

	for (uint32_t i = 0; i < AMIGA_VOICES; i++)
	{
		for (uint32_t y = 0; y < 48; y++)
		{
			for (uint32_t x = 0; x < 10; x++)
				dstPtr[x] = srcPtr[x];

			srcPtr -= SCREEN_W;
			dstPtr += 10;
		}

		srcPtr += (SCREEN_W * 48) + 72;
	}
}

void renderVuMeters(void)
{
	const uint32_t *srcPtr;
	uint32_t h, *dstPtr;

	if (editor.ui.samplerScreenShown || editor.isWAVRendering || editor.isSMPRendering)
		return;

	fillToVuMetersBgBuffer();
	
	dstPtr = &video.frameBuffer[(187 * SCREEN_W) + 55];
	for (uint32_t i = 0; i < AMIGA_VOICES; i++)
	{
		if (config.realVuMeters)
			h = editor.realVuMeterVolumes[i];
		else
			h = editor.vuMeterVolumes[i];

		if (h > 48)
			h = 48;

		srcPtr = vuMeterBMP;
		for (uint32_t y = 0; y < h; y++)
		{
			for (uint32_t x = 0; x < 10; x++)
				dstPtr[x] = srcPtr[x];

			srcPtr += 10;
			dstPtr -= SCREEN_W;
		}

		dstPtr += (SCREEN_W * h) + 72;
	}
}

void updateSongInfo1(void) // left side of screen, when Disk Op. is hidden
{
	moduleSample_t *currSample;

	if (editor.ui.diskOpScreenShown)
		return;

	currSample = &modEntry->samples[editor.currSample];

	if (editor.ui.updateSongPos)
	{
		editor.ui.updateSongPos = false;
		printThreeDecimalsBg(72, 3, *editor.currPosDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateSongPattern)
	{
		editor.ui.updateSongPattern = false;
		printTwoDecimalsBg(80, 14, *editor.currPatternDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateSongLength)
	{
		editor.ui.updateSongLength = false;
		if (!editor.isWAVRendering)
			printThreeDecimalsBg(72, 25, *editor.currLengthDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateCurrSampleFineTune)
	{
		editor.ui.updateCurrSampleFineTune = false;

		if (!editor.isWAVRendering)
		{
			if (currSample->fineTune >= 8)
			{
				charOutBg(80, 36, '-', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
				charOutBg(88, 36, '0' + (0x10 - (currSample->fineTune & 0xF)), video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			}
			else if (currSample->fineTune > 0)
			{
				charOutBg(80, 36, '+', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
				charOutBg(88, 36, '0' + (currSample->fineTune & 0xF), video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			}
			else
			{
				charOutBg(80, 36, ' ', video.palette[PAL_GENBKG], video.palette[PAL_GENBKG]);
				charOutBg(88, 36, '0', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			}
		}
	}

	if (editor.ui.updateCurrSampleNum)
	{
		editor.ui.updateCurrSampleNum = false;
		if (!editor.isWAVRendering)
		{
			printTwoHexBg(80, 47,
				editor.sampleZero ? 0 : ((*editor.currSampleDisp) + 1), video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}

	if (editor.ui.updateCurrSampleVolume)
	{
		editor.ui.updateCurrSampleVolume = false;
		if (!editor.isWAVRendering)
			printTwoHexBg(80, 58, *currSample->volumeDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateCurrSampleLength)
	{
		editor.ui.updateCurrSampleLength = false;
		if (!editor.isWAVRendering)
			printFourHexBg(64, 69, *currSample->lengthDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateCurrSampleRepeat)
	{
		editor.ui.updateCurrSampleRepeat = false;
		printFourHexBg(64, 80, *currSample->loopStartDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateCurrSampleReplen)
	{
		editor.ui.updateCurrSampleReplen = false;
		printFourHexBg(64, 91, *currSample->loopLengthDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}
}

void updateSongInfo2(void) // two middle rows of screen, always present
{
	char tempChar;
	int32_t secs, MI_TimeM, MI_TimeS, x, i;
	moduleSample_t *currSample;

	if (editor.ui.updateStatusText)
	{
		editor.ui.updateStatusText = false;

		// clear background
		textOutBg(88, 127, "                 ", video.palette[PAL_GENBKG], video.palette[PAL_GENBKG]);

		// render status text
		if (!editor.errorMsgActive && editor.blockMarkFlag && !editor.ui.askScreenShown
			&& !editor.ui.clearScreenShown && !editor.swapChannelFlag)
		{
			textOut(88, 127, "MARK BLOCK", video.palette[PAL_GENTXT]);
			charOut(192, 127, '-', video.palette[PAL_GENTXT]);

			editor.blockToPos = modEntry->currRow;
			if (editor.blockFromPos >= editor.blockToPos)
			{
				printTwoDecimals(176, 127, editor.blockToPos, video.palette[PAL_GENTXT]);
				printTwoDecimals(200, 127, editor.blockFromPos, video.palette[PAL_GENTXT]);
			}
			else
			{
				printTwoDecimals(176, 127, editor.blockFromPos, video.palette[PAL_GENTXT]);
				printTwoDecimals(200, 127, editor.blockToPos, video.palette[PAL_GENTXT]);
			}
		}
		else
		{
			textOut(88, 127, editor.ui.statusMessage, video.palette[PAL_GENTXT]);
		}
	}

	if (editor.ui.updateSongBPM)
	{
		editor.ui.updateSongBPM = false;
		if (!editor.ui.samplerScreenShown)
			printThreeDecimalsBg(32, 123, modEntry->currBPM, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateCurrPattText)
	{
		editor.ui.updateCurrPattText = false;
		if (!editor.ui.samplerScreenShown)
			printTwoDecimalsBg(8, 127, *editor.currEditPatternDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateTrackerFlags)
	{
		editor.ui.updateTrackerFlags = false;

		charOutBg(1, 113, ' ', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		charOutBg(8, 113, ' ', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);

		if (editor.autoInsFlag)
		{
			charOut(0, 113, 'I', video.palette[PAL_GENTXT]);

			// in Amiga PT, "auto insert" 9 means 0
			if (editor.autoInsSlot == 9)
				charOut(8, 113, '0', video.palette[PAL_GENTXT]);
			else
				charOut(8, 113, '1' + editor.autoInsSlot, video.palette[PAL_GENTXT]);
		}

		charOutBg(1, 102, ' ', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		if (editor.metroFlag)
			charOut(0, 102, 'M', video.palette[PAL_GENTXT]);

		charOutBg(16, 102, ' ', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		if (editor.multiFlag)
			charOut(16, 102, 'M', video.palette[PAL_GENTXT]);

		charOutBg(24, 102, '0' + editor.editMoveAdd, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);

		charOutBg(311, 128, ' ', video.palette[PAL_GENBKG], video.palette[PAL_GENBKG]);
		if (editor.pNoteFlag == 1)
		{
			video.frameBuffer[(129 * SCREEN_W) + 314] = video.palette[PAL_GENTXT];
			video.frameBuffer[(129 * SCREEN_W) + 315] = video.palette[PAL_GENTXT];
		}
		else if (editor.pNoteFlag == 2)
		{
			video.frameBuffer[(128 * SCREEN_W) + 314] = video.palette[PAL_GENTXT];
			video.frameBuffer[(128 * SCREEN_W) + 315] = video.palette[PAL_GENTXT];
			video.frameBuffer[(130 * SCREEN_W) + 314] = video.palette[PAL_GENTXT];
			video.frameBuffer[(130 * SCREEN_W) + 315] = video.palette[PAL_GENTXT];
		}
	}

	// playback timer

	secs = ((editor.musicTime >> 8) * 5) >> 9;
	secs -= ((secs / 3600) * 3600);

	if (secs <= 5999) // below 99 minutes 59 seconds
	{
		MI_TimeM = secs / 60;
		MI_TimeS = secs - (MI_TimeM * 60);

		// xx:xx
		printTwoDecimalsBg(272, 102, MI_TimeM, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		printTwoDecimalsBg(296, 102, MI_TimeS, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}
	else
	{
		// 99:59
		printTwoDecimalsBg(272, 102, 99, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		printTwoDecimalsBg(296, 102, 59, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateSongName)
	{
		editor.ui.updateSongName = false;
		for (x = 0; x < 20; x++)
		{
			tempChar = modEntry->head.moduleTitle[x];
			if (tempChar == '\0')
				tempChar = '_';

			charOutBg(104 + (x * FONT_CHAR_W), 102, tempChar, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}

	if (editor.ui.updateCurrSampleName)
	{
		editor.ui.updateCurrSampleName = false;
		currSample = &modEntry->samples[editor.currSample];

		for (x = 0; x < 22; x++)
		{
			tempChar = currSample->text[x];
			if (tempChar == '\0')
				tempChar = '_';

			charOutBg(104 + (x * FONT_CHAR_W), 113, tempChar, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}

	if (editor.ui.updateSongSize)
	{
		editor.ui.updateSongSize = false;

		// clear background
		textOutBg(264, 123, "      ", video.palette[PAL_GENBKG], video.palette[PAL_GENBKG]);

		// calculate module length
		uint32_t totalSampleDataSize = 0;
		for (i = 0; i < MOD_SAMPLES; i++)
			totalSampleDataSize += modEntry->samples[i].length;

		uint32_t totalPatterns = 0;
		for (i = 0; i < MOD_ORDERS; i++)
		{
			if (modEntry->head.order[i] > totalPatterns)
				totalPatterns = modEntry->head.order[i];
		}

		uint32_t moduleSize = 2108 + (totalPatterns * 1024) + totalSampleDataSize;
		if (moduleSize > 999999)
		{
			charOut(304, 123, 'K', video.palette[PAL_GENTXT]);
			printFourDecimals(272, 123, moduleSize / 1000, video.palette[PAL_GENTXT]);
		}
		else
		{
			printSixDecimals(264, 123, moduleSize, video.palette[PAL_GENTXT]);
		}
	}

	if (editor.ui.updateSongTiming)
	{
		editor.ui.updateSongTiming = false;
		textOutBg(288, 130, (editor.timingMode == TEMPO_MODE_CIA) ? "CIA" : "VBL", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}
}

void updateCursorPos(void)
{
	if (!editor.ui.samplerScreenShown)
		setSpritePos(SPRITE_PATTERN_CURSOR, cursorPosTable[editor.cursor.pos], 188);
}

void updateSampler(void)
{
	int32_t tmpSampleOffset;
	moduleSample_t *s;

	if (!editor.ui.samplerScreenShown)
		return;

	assert(editor.currSample >= 0 && editor.currSample <= 30);
	s = &modEntry->samples[editor.currSample];

	// update 9xx offset
	if (mouse.y >= 138 && mouse.y <= 201 && mouse.x >= 3 && mouse.x <= 316)
	{
		if (!editor.ui.samplerVolBoxShown && !editor.ui.samplerFiltersBoxShown && s->length > 0)
		{
			tmpSampleOffset = (scr2SmpPos(mouse.x-3) + (1 << 7)) >> 8; // rounded
			tmpSampleOffset = 0x900 + CLAMP(tmpSampleOffset, 0x00, 0xFF);

			if (tmpSampleOffset != editor.ui.lastSampleOffset)
			{
				editor.ui.lastSampleOffset = tmpSampleOffset;
				editor.ui.update9xxPos = true;
			}
		}
	}

	// display 9xx offset
	if (editor.ui.update9xxPos)
	{
		editor.ui.update9xxPos = false;
		printThreeHexBg(288, 247, editor.ui.lastSampleOffset, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateResampleNote)
	{
		editor.ui.updateResampleNote = false;

		// show resample note
		if (editor.ui.changingSmpResample)
		{
			textOutBg(288, 236, "---", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
		else
		{
			assert(editor.resampleNote < 36);
			textOutBg(288, 236,
				config.accidental ? noteNames2[editor.resampleNote] : noteNames1[editor.resampleNote],
				video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}

	if (editor.ui.samplerVolBoxShown)
	{
		if (editor.ui.updateVolFromText)
		{
			editor.ui.updateVolFromText = false;
			printThreeDecimalsBg(176, 157, *editor.vol1Disp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateVolToText)
		{
			editor.ui.updateVolToText = false;
			printThreeDecimalsBg(176, 168, *editor.vol2Disp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}
	else if (editor.ui.samplerFiltersBoxShown)
	{
		if (editor.ui.updateLPText)
		{
			editor.ui.updateLPText = false;
			printFourDecimalsBg(168, 157, *editor.lpCutOffDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateHPText)
		{
			editor.ui.updateHPText = false;
			printFourDecimalsBg(168, 168, *editor.hpCutOffDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateNormFlag)
		{
			editor.ui.updateNormFlag = false;

			if (editor.normalizeFiltersFlag)
				textOutBg(208, 179, "YES", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			else
				textOutBg(208, 179, "NO ", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}
}

void showVolFromSlider(void)
{
	uint32_t *dstPtr, pixel, bgPixel, sliderStart, sliderEnd;

	sliderStart = ((editor.vol1 * 3) + 5) / 10;
	sliderEnd  = sliderStart + 4;
	pixel = video.palette[PAL_QADSCP];
	bgPixel = video.palette[PAL_BACKGRD];
	dstPtr = &video.frameBuffer[(158 * SCREEN_W) + 105];

	for (uint32_t y = 0; y < 3; y++)
	{
		for (uint32_t x = 0; x < 65; x++)
		{
			if (x >= sliderStart && x <= sliderEnd)
				dstPtr[x] = pixel;
			else
				dstPtr[x] = bgPixel;
		}

		dstPtr += SCREEN_W;
	}
}

void showVolToSlider(void)
{
	uint32_t *dstPtr, pixel, bgPixel, sliderStart, sliderEnd;

	sliderStart = ((editor.vol2 * 3) + 5) / 10;
	sliderEnd = sliderStart + 4;
	pixel = video.palette[PAL_QADSCP];
	bgPixel = video.palette[PAL_BACKGRD];
	dstPtr = &video.frameBuffer[(169 * SCREEN_W) + 105];

	for (uint32_t y = 0; y < 3; y++)
	{
		for (uint32_t x = 0; x < 65; x++)
		{
			if (x >= sliderStart && x <= sliderEnd)
				dstPtr[x] = pixel;
			else
				dstPtr[x] = bgPixel;
		}

		dstPtr += SCREEN_W;
	}
}

void renderSamplerVolBox(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	srcPtr = samplerVolumeBMP;
	dstPtr = &video.frameBuffer[(154 * SCREEN_W) + 72];

	for (uint32_t y = 0; y < 33; y++)
	{
		memcpy(dstPtr, srcPtr, 136 * sizeof (int32_t));

		srcPtr += 136;
		dstPtr += SCREEN_W;
	}

	editor.ui.updateVolFromText = true;
	editor.ui.updateVolToText = true;
	showVolFromSlider();
	showVolToSlider();

	// hide loop sprites
	hideSprite(SPRITE_LOOP_PIN_LEFT);
	hideSprite(SPRITE_LOOP_PIN_RIGHT);
}

void removeSamplerVolBox(void)
{
	displaySample();
}

void renderSamplerFiltersBox(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	srcPtr = samplerFiltersBMP;
	dstPtr = &video.frameBuffer[(154 * SCREEN_W) + 65];

	for (uint32_t y = 0; y < 33; y++)
	{
		memcpy(dstPtr, srcPtr, 186 * sizeof (int32_t));

		srcPtr += 186;
		dstPtr += SCREEN_W;
	}

	textOut(200, 157, "HZ", video.palette[PAL_GENTXT]);
	textOut(200, 168, "HZ", video.palette[PAL_GENTXT]);

	editor.ui.updateLPText = true;
	editor.ui.updateHPText = true;
	editor.ui.updateNormFlag = true;

	// hide loop sprites
	hideSprite(SPRITE_LOOP_PIN_LEFT);
	hideSprite(SPRITE_LOOP_PIN_RIGHT);
}

void removeSamplerFiltersBox(void)
{
	displaySample();
}

void renderDiskOpScreen(void)
{
	memcpy(video.frameBuffer, diskOpScreenBMP, (99 * 320) * sizeof (int32_t));

	editor.ui.updateDiskOpPathText = true;
	editor.ui.updatePackText = true;
	editor.ui.updateSaveFormatText = true;
	editor.ui.updateLoadMode = true;
	editor.ui.updateDiskOpFileList = true;
}

void updateDiskOp(void)
{
	char tmpChar;
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	if (!editor.ui.diskOpScreenShown || editor.ui.posEdScreenShown)
		return;

	if (editor.ui.updateDiskOpFileList)
	{
		editor.ui.updateDiskOpFileList = false;
		diskOpRenderFileList();
	}

	if (editor.ui.updateLoadMode)
	{
		editor.ui.updateLoadMode = false;

		// clear backgrounds
		charOutBg(147,  3, ' ', video.palette[PAL_GENBKG], video.palette[PAL_GENBKG]);
		charOutBg(147, 14, ' ', video.palette[PAL_GENBKG], video.palette[PAL_GENBKG]);

		// draw load mode arrow

		srcPtr = arrowBMP;
		dstPtr = &video.frameBuffer[(((11 * editor.diskop.mode) + 3) * SCREEN_W) + 148];

		for (uint32_t y = 0; y < 5; y++)
		{
			for (uint32_t x = 0; x < 6; x++)
				dstPtr[x] = srcPtr[x];

			srcPtr += 6;
			dstPtr += SCREEN_W;
		}
	}

	if (editor.ui.updatePackText)
	{
		editor.ui.updatePackText = false;
		textOutBg(120, 3, editor.diskop.modPackFlg ? "ON " : "OFF", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateSaveFormatText)
	{
		editor.ui.updateSaveFormatText = false;
		     if (editor.diskop.smpSaveType == DISKOP_SMP_WAV) textOutBg(120, 14, "WAV", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		else if (editor.diskop.smpSaveType == DISKOP_SMP_IFF) textOutBg(120, 14, "IFF", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		else if (editor.diskop.smpSaveType == DISKOP_SMP_RAW) textOutBg(120, 14, "RAW", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
	}

	if (editor.ui.updateDiskOpPathText)
	{
		editor.ui.updateDiskOpPathText = false;

		// print disk op. path
		for (uint32_t i = 0; i < 26; i++)
		{
			tmpChar = editor.currPath[editor.textofs.diskOpPath+i];
			if (tmpChar == '\0')
				tmpChar = '_';

			charOutBg(24 + (i * FONT_CHAR_W), 25, tmpChar, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}
}

void updatePosEd(void)
{
	int16_t posEdPosition;
	int32_t x, y, y2;
	uint32_t *dstPtr, bgPixel;

	if (!editor.ui.posEdScreenShown || !editor.ui.updatePosEd)
		return;

	editor.ui.updatePosEd = false;

	if (!editor.ui.disablePosEd)
	{
		bgPixel = video.palette[PAL_BACKGRD];

		posEdPosition = modEntry->currOrder;
		if (posEdPosition > modEntry->head.orderCount-1)
			posEdPosition = modEntry->head.orderCount-1;

		// top five
		for (y = 0; y < 5; y++)
		{
			if (posEdPosition-(5-y) >= 0)
			{
				printThreeDecimalsBg(128, 23+(y*6), posEdPosition-(5-y), video.palette[PAL_QADSCP], video.palette[PAL_BACKGRD]);
				printTwoDecimalsBg(160, 23+(y*6), modEntry->head.order[posEdPosition-(5-y)], video.palette[PAL_QADSCP], video.palette[PAL_BACKGRD]);
			}
			else
			{
				dstPtr = &video.frameBuffer[((23+(y*6)) * SCREEN_W) + 128];
				for (y2 = 0; y2 < 5; y2++)
				{
					for (x = 0; x < FONT_CHAR_W*22; x++)
						dstPtr[x] = bgPixel;

					dstPtr += SCREEN_W;
				}
			}
		}

		// middle
		printThreeDecimalsBg(128, 53, posEdPosition, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		printTwoDecimalsBg(160, 53, *editor.currPosEdPattDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);

		// bottom six
		for (y = 0; y < 6; y++)
		{
			if (posEdPosition+y < modEntry->head.orderCount-1)
			{
				printThreeDecimalsBg(128, 59+(y*6), posEdPosition+(y+1), video.palette[PAL_QADSCP], video.palette[PAL_BACKGRD]);
				printTwoDecimalsBg(160, 59+(y*6), modEntry->head.order[posEdPosition+(y+1)], video.palette[PAL_QADSCP], video.palette[PAL_BACKGRD]);
			}
			else
			{
				dstPtr = &video.frameBuffer[((59+(y*6)) * SCREEN_W) + 128];
				for (y2 = 0; y2 < 5; y2++)
				{
					for (x = 0; x < FONT_CHAR_W*22; x++)
						dstPtr[x] = bgPixel;

					dstPtr += SCREEN_W;
				}
			}
		}

		// kludge to fix bottom part of text edit marker in pos ed
		if (editor.ui.editTextFlag && editor.ui.editObject == PTB_PE_PATT)
			renderTextEditMarker();
	}
}

void renderPosEdScreen(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	srcPtr = posEdBMP;
	dstPtr = &video.frameBuffer[120];

	for (uint32_t y = 0; y < 99; y++)
	{
		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));

		srcPtr += 200;
		dstPtr += SCREEN_W;
	}
}

void renderMuteButtons(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr, srcPitch;

	if (editor.ui.diskOpScreenShown || editor.ui.posEdScreenShown)
		return;

	dstPtr = &video.frameBuffer[(3 * SCREEN_W) + 310];
	for (uint32_t i = 0; i < AMIGA_VOICES; i++)
	{
		if (editor.muted[i])
		{
			srcPtr = &muteButtonsBMP[i * (6 * 7)];
			srcPitch = 7;
		}
		else
		{
			srcPtr = &trackerFrameBMP[((3 + (i * 11)) * SCREEN_W) + 310];
			srcPitch = SCREEN_W;
		}

		for (uint32_t y = 0; y < 6; y++)
		{
			for (uint32_t x = 0; x < 7; x++)
				dstPtr[x] = srcPtr[x];

			srcPtr += srcPitch;
			dstPtr += SCREEN_W;
		}

		dstPtr += SCREEN_W * 5;
	}
}

void renderClearScreen(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	editor.ui.disablePosEd = true;
	editor.ui.disableVisualizer = true;

	srcPtr = clearDialogBMP;
	dstPtr = &video.frameBuffer[(51 * SCREEN_W) + 160];

	for (uint32_t y = 0; y < 39; y++)
	{
		memcpy(dstPtr, srcPtr, 104 * sizeof (int32_t));

		srcPtr += 104;
		dstPtr += SCREEN_W;
	}
}

void removeClearScreen(void)
{
	displayMainScreen();

	editor.ui.disablePosEd = false;
	editor.ui.disableVisualizer = false;
}

void updateCurrSample(void)
{
	editor.ui.updateCurrSampleName = true;
	editor.ui.updateSongSize = true;

	if (!editor.ui.diskOpScreenShown)
	{
		editor.ui.updateCurrSampleFineTune = true;
		editor.ui.updateCurrSampleNum = true;
		editor.ui.updateCurrSampleVolume = true;
		editor.ui.updateCurrSampleLength = true;
		editor.ui.updateCurrSampleRepeat = true;
		editor.ui.updateCurrSampleReplen = true;
	}

	if (editor.ui.samplerScreenShown)
		redrawSample();

	updateSamplePos();
	recalcChordLength();

	editor.sampler.tmpLoopStart = 0;
	editor.sampler.tmpLoopLength = 0;
}

void updatePatternData(void)
{
	if (editor.ui.updatePatternData)
	{
		editor.ui.updatePatternData = false;
		if (!editor.ui.samplerScreenShown)
			redrawPattern();
	}
}

void removeTextEditMarker(void)
{
	uint32_t *dstPtr, pixel;

	if (!editor.ui.editTextFlag)
		return;

	dstPtr = &video.frameBuffer[((editor.ui.lineCurY - 1) * SCREEN_W) + (editor.ui.lineCurX - 4)];

	if (editor.ui.editObject == PTB_PE_PATT)
	{
		// position editor text editing

		pixel = video.palette[PAL_GENBKG2];
		for (uint32_t x = 0; x < 7; x++)
			dstPtr[x] = pixel;

		// no need to clear the second row of pixels

		editor.ui.updatePosEd = true;
	}
	else
	{
		// all others

		pixel = video.palette[PAL_GENBKG];
		for (uint32_t y = 0; y < 2; y++)
		{
			for (uint32_t x = 0; x < 7; x++)
				dstPtr[x] = pixel;

			dstPtr += SCREEN_W;
		}
	}
}

void renderTextEditMarker(void)
{
	uint32_t *dstPtr, pixel;

	if (!editor.ui.editTextFlag)
		return;

	dstPtr = &video.frameBuffer[((editor.ui.lineCurY - 1) * SCREEN_W) + (editor.ui.lineCurX - 4)];
	pixel = video.palette[PAL_TEXTMARK];

	for (uint32_t y = 0; y < 2; y++)
	{
		for (uint32_t x = 0; x < 7; x++)
			dstPtr[x] = pixel;

		dstPtr += SCREEN_W;
	}
}

static void sendMouseButtonUpEvent(uint8_t button)
{
	SDL_Event event;

	memset(&event, 0, sizeof (event));

	event.type = SDL_MOUSEBUTTONUP;
	event.button.button = button;

	SDL_PushEvent(&event);
}

void handleLastGUIObjectDown(void)
{
	bool testMouseButtonRelease = false;

	if (editor.ui.sampleMarkingPos >= 0)
	{
		samplerSamplePressed(MOUSE_BUTTON_HELD);
		testMouseButtonRelease = true;
	}

	if (editor.ui.forceSampleDrag)
	{
		samplerBarPressed(MOUSE_BUTTON_HELD);
		testMouseButtonRelease = true;
	}

	if (editor.ui.forceSampleEdit)
	{
		samplerEditSample(MOUSE_BUTTON_HELD);
		testMouseButtonRelease = true;
	}

	if (editor.ui.forceVolDrag)
	{
		volBoxBarPressed(MOUSE_BUTTON_HELD);
		testMouseButtonRelease = true;
	}

	/* Hack to send "mouse button up" events if we released the mouse button(s)
	** outside of the window...
	*/
	if (testMouseButtonRelease)
	{
		if (mouse.x < 0 || mouse.x >= SCREEN_W || mouse.y < 0 || mouse.y >= SCREEN_H)
		{
			if (mouse.leftButtonPressed && !(mouse.buttonState & SDL_BUTTON_LMASK))
				sendMouseButtonUpEvent(SDL_BUTTON_LEFT);

			if (mouse.rightButtonPressed && !(mouse.buttonState & SDL_BUTTON_RMASK))
				sendMouseButtonUpEvent(SDL_BUTTON_RIGHT);
		}
	}
}

void updateVisualizer(void)
{
	const uint32_t *srcPtr;
	int32_t tmpVol;
	uint32_t *dstPtr, pixel;

	if (editor.ui.disableVisualizer || editor.ui.diskOpScreenShown ||
		editor.ui.posEdScreenShown  || editor.ui.editOpScreenShown ||
		editor.ui.aboutScreenShown  || editor.ui.askScreenShown    ||
		editor.isWAVRendering)
	{
		return;
	}

	if (editor.ui.visualizerMode == VISUAL_SPECTRUM)
	{
		// spectrum analyzer

		dstPtr = &video.frameBuffer[(59 * SCREEN_W) + 129];
		for (uint32_t i = 0; i < SPECTRUM_BAR_NUM; i++)
		{
			srcPtr = spectrumAnaBMP;
			pixel = video.palette[PAL_GENBKG];

			tmpVol = editor.spectrumVolumes[i];
			if (tmpVol > SPECTRUM_BAR_HEIGHT)
				tmpVol = SPECTRUM_BAR_HEIGHT;

			for (int32_t y = SPECTRUM_BAR_HEIGHT-1; y >= 0; y--)
			{
				if (y < tmpVol)
					pixel = srcPtr[y];

				for (uint32_t x = 0; x < SPECTRUM_BAR_WIDTH; x++)
					dstPtr[x] = pixel;

				dstPtr += SCREEN_W;
			}

			dstPtr -= (SCREEN_W * SPECTRUM_BAR_HEIGHT) - (SPECTRUM_BAR_WIDTH + 2);
		}
	}
	else
	{
		drawScopes();
	}
}

void renderQuadrascopeBg(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	srcPtr = &trackerFrameBMP[(44 * SCREEN_W) + 120];
	dstPtr = &video.frameBuffer[(44 * SCREEN_W) + 120];

	for (uint32_t y = 0; y < 55; y++)
	{
		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));

		srcPtr += SCREEN_W;
		dstPtr += SCREEN_W;
	}

	for (uint32_t i = 0; i < AMIGA_VOICES; i++)
		scopeExt[i].emptyScopeDrawn = false;
}

void renderSpectrumAnalyzerBg(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	srcPtr = spectrumVisualsBMP;
	dstPtr = &video.frameBuffer[(44 * SCREEN_W) + 120];

	for (uint32_t y = 0; y < 55; y++)
	{
		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));

		srcPtr += 200;
		dstPtr += SCREEN_W;
	}
}

void renderAboutScreen(void)
{
	char verString[16];
	const uint32_t *srcPtr;
	uint32_t verStringX, *dstPtr;

	if (!editor.ui.aboutScreenShown || editor.ui.diskOpScreenShown || editor.ui.posEdScreenShown || editor.ui.editOpScreenShown)
		return;

	srcPtr = aboutScreenBMP;
	dstPtr = &video.frameBuffer[(44 * SCREEN_W) + 120];

	for (uint32_t y = 0; y < 55; y++)
	{
		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));

		srcPtr += 200;
		dstPtr += SCREEN_W;
	}

	// draw version string

	sprintf(verString, "v%s", PROG_VER_STR);
	verStringX = 260 + (((63 - ((uint32_t)strlen(verString) * (FONT_CHAR_W - 1))) + 1) / 2);
	textOutTight(verStringX, 67, verString, video.palette[PAL_GENBKG2]);
}

void renderEditOpMode(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	// select what character box to render

	switch (editor.ui.editOpScreen)
	{
		default:
		case 0:
			srcPtr = &editOpModeCharsBMP[editor.sampleAllFlag ? EDOP_MODE_BMP_A_OFS : EDOP_MODE_BMP_S_OFS];
		break;

		case 1:
		{
			     if (editor.trackPattFlag == 0) srcPtr = &editOpModeCharsBMP[EDOP_MODE_BMP_T_OFS];
			else if (editor.trackPattFlag == 1) srcPtr = &editOpModeCharsBMP[EDOP_MODE_BMP_P_OFS];
			else srcPtr = &editOpModeCharsBMP[EDOP_MODE_BMP_S_OFS];
		}
		break;

		case 2:
			srcPtr = &editOpModeCharsBMP[editor.halfClipFlag ? EDOP_MODE_BMP_C_OFS : EDOP_MODE_BMP_H_OFS];
		break;

		case 3:
			srcPtr = (editor.newOldFlag == 0) ? &editOpModeCharsBMP[EDOP_MODE_BMP_N_OFS] : &editOpModeCharsBMP[EDOP_MODE_BMP_O_OFS];
		break;
	}

	// render it...

	dstPtr = &video.frameBuffer[(47 * SCREEN_W) + 310];
	for (uint32_t y = 0; y < 6; y++)
	{
		for (uint32_t x = 0; x < 7; x++)
			dstPtr[x] = srcPtr[x];

		srcPtr += 7;
		dstPtr += SCREEN_W;
	}
}

void renderEditOpScreen(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	// select which background to render
	switch (editor.ui.editOpScreen)
	{
		default:
		case 0: srcPtr = editOpScreen1BMP; break;
		case 1: srcPtr = editOpScreen2BMP; break;
		case 2: srcPtr = editOpScreen3BMP; break;
		case 3: srcPtr = editOpScreen4BMP; break;
	}

	// render background
	dstPtr = &video.frameBuffer[(44 * SCREEN_W) + 120];
	for (uint32_t y = 0; y < 55; y++)
	{
		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));

		srcPtr += 200;
		dstPtr += SCREEN_W;
	}

	renderEditOpMode();

	// render text and content
	if (editor.ui.editOpScreen == 0)
	{
		textOut(128, 47, "  TRACK      PATTERN  ", video.palette[PAL_GENTXT]);
	}
	else if (editor.ui.editOpScreen == 1)
	{
		textOut(128, 47, "  RECORD     SAMPLES  ", video.palette[PAL_GENTXT]);

		editor.ui.updateRecordText = true;
		editor.ui.updateQuantizeText = true;
		editor.ui.updateMetro1Text = true;
		editor.ui.updateMetro2Text = true;
		editor.ui.updateFromText = true;
		editor.ui.updateKeysText = true;
		editor.ui.updateToText = true;
	}
	else if (editor.ui.editOpScreen == 2)
	{
		textOut(128, 47, "    SAMPLE EDITOR     ", video.palette[PAL_GENTXT]);
		charOut(272, 91, '%', video.palette[PAL_GENTXT]); // for Volume text

		editor.ui.updatePosText = true;
		editor.ui.updateModText = true;
		editor.ui.updateVolText = true;
	}
	else if (editor.ui.editOpScreen == 3)
	{
		textOut(128, 47, " SAMPLE CHORD EDITOR  ", video.palette[PAL_GENTXT]);

		editor.ui.updateLengthText = true;
		editor.ui.updateNote1Text = true;
		editor.ui.updateNote2Text = true;
		editor.ui.updateNote3Text = true;
		editor.ui.updateNote4Text = true;
	}
}

void renderMOD2WAVDialog(void)
{
	const uint32_t *srcPtr;
	uint32_t *dstPtr;

	srcPtr = mod2wavBMP;
	dstPtr = &video.frameBuffer[(27 * SCREEN_W) + 64];

	for (uint32_t y = 0; y < 48; y++)
	{
		memcpy(dstPtr, srcPtr, 192 * sizeof (int32_t));

		srcPtr += 192;
		dstPtr += SCREEN_W;
	}
}

void updateMOD2WAVDialog(void)
{
	int32_t barLength, percent;
	uint32_t *dstPtr, bgPixel, pixel;

	if (!editor.ui.updateMod2WavDialog)
		return;

	editor.ui.updateMod2WavDialog = false;

	if (editor.isWAVRendering)
	{
		if (editor.ui.mod2WavFinished)
		{
			editor.ui.mod2WavFinished = false;

			resetSong();
			pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);

			if (editor.abortMod2Wav)
			{
				displayErrorMsg("MOD2WAV ABORTED !");
			}
			else
			{
				displayMsg("MOD RENDERED !");
				setMsgPointer();
			}

			editor.isWAVRendering = false;
			displayMainScreen();
		}
		else
		{
			// render progress bar

			percent = (uint8_t)((modEntry->rowsCounter * 100) / modEntry->rowsInTotal);
			if (percent > 100)
				percent = 100;

			barLength = ((percent * 180) + 50) / 100;
			dstPtr = &video.frameBuffer[(42 * SCREEN_W) + 70];
			pixel = video.palette[PAL_GENBKG2];
			bgPixel = video.palette[PAL_BORDER];

			for (int32_t y = 0; y < 11; y++)
			{
				for (int32_t x = 0; x < 180; x++)
				{
					uint32_t color = bgPixel;
					if (x < barLength)
						color = pixel;

					dstPtr[x] = color;
				}

				dstPtr += SCREEN_W;
			}

			// render percentage
			pixel = video.palette[PAL_GENTXT];
			if (percent > 99)
				printThreeDecimals(144, 45, percent, pixel);
			else
				printTwoDecimals(152, 45, percent, pixel);

			charOut(168, 45, '%', pixel);
		}
	}
}

void updateEditOp(void)
{
	if (!editor.ui.editOpScreenShown || editor.ui.posEdScreenShown || editor.ui.diskOpScreenShown)
		return;

	if (editor.ui.editOpScreen == 1)
	{
		if (editor.ui.updateRecordText)
		{
			editor.ui.updateRecordText = false;
			textOutBg(176, 58, (editor.recordMode == RECORD_PATT) ? "PATT" : "SONG", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateQuantizeText)
		{
			editor.ui.updateQuantizeText = false;
			printTwoDecimalsBg(192, 69, *editor.quantizeValueDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateMetro1Text)
		{
			editor.ui.updateMetro1Text = false;
			printTwoDecimalsBg(168, 80, *editor.metroSpeedDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateMetro2Text)
		{
			editor.ui.updateMetro2Text = false;
			printTwoDecimalsBg(192, 80, *editor.metroChannelDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateFromText)
		{
			editor.ui.updateFromText = false;
			printTwoHexBg(264, 80, *editor.sampleFromDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateKeysText)
		{
			editor.ui.updateKeysText = false;
			textOutBg(160, 91, editor.multiFlag ? "MULTI " : "SINGLE", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateToText)
		{
			editor.ui.updateToText = false;
			printTwoHexBg(264, 91, *editor.sampleToDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}
	else if (editor.ui.editOpScreen == 2)
	{
		if (editor.ui.updateMixText)
		{
			editor.ui.updateMixText = false;
			if (editor.mixFlag)
			{
				textOutBg(128, 47, editor.mixText, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
				textOutBg(248, 47, "  ", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			}
			else
			{
				textOutBg(128, 47, "    SAMPLE EDITOR     ", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			}
		}

		if (editor.ui.updatePosText)
		{
			editor.ui.updatePosText = false;
			printFourHexBg(248, 58, *editor.samplePosDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateModText)
		{
			editor.ui.updateModText = false;
			printThreeDecimalsBg(256, 69,
				(editor.modulateSpeed < 0) ? (0 - editor.modulateSpeed) : editor.modulateSpeed,
				video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);

			if (editor.modulateSpeed < 0)
				charOutBg(248, 69, '-', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			else
				charOutBg(248, 69, ' ', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateVolText)
		{
			editor.ui.updateVolText = false;
			printThreeDecimalsBg(248, 91, *editor.sampleVolDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}
	else if (editor.ui.editOpScreen == 3)
	{
		if (editor.ui.updateLengthText)
		{
			editor.ui.updateLengthText = false;

			// clear background
			textOutBg(168, 91, "    ", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			charOut(198, 91,    ':', video.palette[PAL_GENBKG]);

			if (modEntry->samples[editor.currSample].loopLength > 2 || modEntry->samples[editor.currSample].loopStart >= 2)
			{
				textOut(168, 91, "LOOP", video.palette[PAL_GENTXT]);
			}
			else
			{
				printFourHex(168, 91, *editor.chordLengthDisp, video.palette[PAL_GENTXT]); // chord max length
				charOut(198, 91, (editor.chordLengthMin) ? '.' : ':', video.palette[PAL_GENTXT]); // min/max flag
			}
		}

		if (editor.ui.updateNote1Text)
		{
			editor.ui.updateNote1Text = false;
			if (editor.note1 > 35)
				textOutBg(256, 58, "---", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			else
				textOutBg(256, 58, config.accidental ? noteNames2[editor.note1] : noteNames1[editor.note1],
					video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateNote2Text)
		{
			editor.ui.updateNote2Text = false;
			if (editor.note2 > 35)
				textOutBg(256, 69, "---", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			else
				textOutBg(256, 69, config.accidental ? noteNames2[editor.note2] : noteNames1[editor.note2],
					video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (editor.ui.updateNote3Text)
		{
			editor.ui.updateNote3Text = false;
			if (editor.note3 > 35)
				textOutBg(256, 80, "---", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			else
				textOutBg(256, 80, config.accidental ? noteNames2[editor.note3] : noteNames1[editor.note3],
					video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
			
		if (editor.ui.updateNote4Text)
		{
			editor.ui.updateNote4Text = false;
			if (editor.note4 > 35)
				textOutBg(256, 91, "---", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
			else
				textOutBg(256, 91, config.accidental ? noteNames2[editor.note4] : noteNames1[editor.note4],
					video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}
}

void displayMainScreen(void)
{
	editor.blockMarkFlag = false;

	editor.ui.updateSongName = true;
	editor.ui.updateSongSize = true;
	editor.ui.updateSongTiming = true;
	editor.ui.updateTrackerFlags = true;
	editor.ui.updateStatusText = true;

	editor.ui.updateCurrSampleName = true;

	if (!editor.ui.diskOpScreenShown)
	{
		editor.ui.updateCurrSampleFineTune = true;
		editor.ui.updateCurrSampleNum = true;
		editor.ui.updateCurrSampleVolume = true;
		editor.ui.updateCurrSampleLength = true;
		editor.ui.updateCurrSampleRepeat = true;
		editor.ui.updateCurrSampleReplen = true;
	}

	if (editor.ui.samplerScreenShown)
	{
		if (!editor.ui.diskOpScreenShown)
			memcpy(video.frameBuffer, trackerFrameBMP, 320 * 121 * sizeof (int32_t));
	}
	else
	{
		if (!editor.ui.diskOpScreenShown)
			memcpy(video.frameBuffer, trackerFrameBMP, 320 * 255 * sizeof (int32_t));
		else
			memcpy(&video.frameBuffer[121 * SCREEN_W], &trackerFrameBMP[121 * SCREEN_W], 320 * 134 * sizeof (int32_t));

		editor.ui.updateSongBPM = true;
		editor.ui.updateCurrPattText = true;
		editor.ui.updatePatternData  = true;
	}

	if (editor.ui.diskOpScreenShown)
	{
		renderDiskOpScreen();
	}
	else
	{
		editor.ui.updateSongPos = true;
		editor.ui.updateSongPattern = true;
		editor.ui.updateSongLength = true;

		// zeroes (can't integrate zeroes in the graphics, the palette entry is above the 2-bit range)
		charOut(64,  3, '0', video.palette[PAL_GENTXT]);
		textOut(64, 14, "00", video.palette[PAL_GENTXT]);

		if (!editor.isWAVRendering)
		{
			charOut(64, 25, '0', video.palette[PAL_GENTXT]);
			textOut(64, 47, "00", video.palette[PAL_GENTXT]);
			textOut(64, 58, "00", video.palette[PAL_GENTXT]);
		}

		if (editor.ui.posEdScreenShown)
		{
			renderPosEdScreen();
			editor.ui.updatePosEd = true;
		}
		else
		{
			if (editor.ui.editOpScreenShown)
			{
				renderEditOpScreen();
			}
			else
			{
				if (editor.ui.aboutScreenShown)
				{
					renderAboutScreen();
				}
				else
				{
					     if (editor.ui.visualizerMode == VISUAL_QUADRASCOPE) renderQuadrascopeBg();
					else if (editor.ui.visualizerMode == VISUAL_SPECTRUM) renderSpectrumAnalyzerBg();
				}
			}

			renderMuteButtons();
		}
	}
}

static void restoreStatusAndMousePointer(void)
{
	editor.errorMsgActive = false;
	editor.errorMsgBlock = false;
	editor.errorMsgCounter = 0;
	pointerSetPreviousMode();
	setPrevStatusMessage();
}

void handleAskNo(void)
{
	editor.ui.pat2SmpDialogShown = false;

	switch (editor.ui.askScreenType)
	{
		case ASK_SAVEMOD_OVERWRITE:
		{
			restoreStatusAndMousePointer();
			saveModule(DONT_CHECK_IF_FILE_EXIST, GIVE_NEW_FILENAME);
		}
		break;

		case ASK_SAVESMP_OVERWRITE:
		{
			restoreStatusAndMousePointer();
			saveSample(DONT_CHECK_IF_FILE_EXIST, GIVE_NEW_FILENAME);
		}
		break;

		case ASK_LOAD_DOWNSAMPLE:
		{
			restoreStatusAndMousePointer();
			extLoadWAVOrAIFFSampleCallback(DONT_DOWNSAMPLE);
		}
		break;

		default:
		{
			restoreStatusAndMousePointer();
			editor.errorMsgActive = true;
			editor.errorMsgBlock = true;
			editor.errorMsgCounter = 0;
			setErrPointer();
		}
		break;
	}

	removeAskDialog();
}

void handleAskYes(void)
{
	char fileName[20 + 4 + 1];
	int8_t *tmpSmpBuffer, oldSample, oldRow;
	int32_t j, newLength, oldSamplesPerTick, loopStart, loopLength;
	uint32_t i;
	moduleSample_t *s;

	switch (editor.ui.askScreenType)
	{
		case ASK_DISCARD_SONG:
		{
			restoreStatusAndMousePointer();
			diskOpLoadFile2();
		}
		break;

		case ASK_DISCARD_SONG_DRAGNDROP:
		{
			restoreStatusAndMousePointer();
			loadDroppedFile2();
		}
		break;

		case ASK_RESTORE_SAMPLE:
		{
			restoreStatusAndMousePointer();
			redoSampleData(editor.currSample);
		}
		break;

		case ASK_PAT2SMP:
		{
			restoreStatusAndMousePointer();

			editor.ui.pat2SmpDialogShown = false;

			editor.pat2SmpBuf = (int16_t *)malloc(MAX_SAMPLE_LEN * sizeof (int16_t));
			if (editor.pat2SmpBuf == NULL)
			{
				statusOutOfMemory();
				return;
			}

			oldRow = editor.songPlaying ? 0 : modEntry->currRow;
			oldSamplesPerTick = samplesPerTick;

			editor.isSMPRendering = true; // this must be set before restartSong()
			storeTempVariables();
			restartSong();
			modEntry->row = oldRow;
			modEntry->currRow = modEntry->row;

			editor.blockMarkFlag = false;
			pointerSetMode(POINTER_MODE_MSG2, NO_CARRY);
			setStatusMessage("RENDERING...", NO_CARRY);
			modSetTempo(modEntry->currBPM);
			editor.pat2SmpPos = 0;

			editor.smpRenderingDone = false;
			while (!editor.smpRenderingDone)
			{
				if (!intMusic())
					editor.smpRenderingDone = true;

				outputAudio(NULL, samplesPerTick);
			}
			editor.isSMPRendering = false;
			resetSong();

			// set back old row and samplesPerTick
			modEntry->row = oldRow;
			modEntry->currRow = modEntry->row;
			mixerSetSamplesPerTick(oldSamplesPerTick);

			// normalize 16-bit samples
			normalize16bitSigned(editor.pat2SmpBuf, MIN(editor.pat2SmpPos, MAX_SAMPLE_LEN));

			s = &modEntry->samples[editor.currSample];

			// quantize to 8-bit
			for (i = 0; i < editor.pat2SmpPos; i++)
				modEntry->sampleData[s->offset+i] = editor.pat2SmpBuf[i] >> 8;

			// clear the rest of the sample
			if (editor.pat2SmpPos < MAX_SAMPLE_LEN)
				memset(&modEntry->sampleData[s->offset+editor.pat2SmpPos], 0, MAX_SAMPLE_LEN - editor.pat2SmpPos);

			// free temp mixing buffer
			free(editor.pat2SmpBuf);

			// zero out sample text
			memset(s->text, 0, sizeof (s->text));

			// set new sample text
			if (editor.pat2SmpHQ)
			{
				strcpy(s->text, "pat2smp (a-3 tune:+5)");
				s->fineTune = 5;
			}
			else
			{
				strcpy(s->text, "pat2smp (f-3 tune:+1)");
				s->fineTune = 1;
			}

			// new sample attributes
			s->length = editor.pat2SmpPos;
			s->volume = 64;
			s->loopStart = 0;
			s->loopLength = 2;

			pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);
			displayMsg("ROWS RENDERED!");
			setMsgPointer();
			editor.samplePos = 0;
			fixSampleBeep(s);
			updateCurrSample();
		}
		break;

		case ASK_SAVE_ALL_SAMPLES:
		{
			editor.errorMsgActive = false;
			editor.errorMsgBlock = false;
			editor.errorMsgCounter = 0;

			oldSample = editor.currSample;
			for (i = 0; i < MOD_SAMPLES; i++)
			{
				editor.currSample = (int8_t)i;
				if (modEntry->samples[i].length > 2)
					saveSample(DONT_CHECK_IF_FILE_EXIST, GIVE_NEW_FILENAME);
			}
			editor.currSample = oldSample;

			displayMsg("SAMPLES SAVED !");
			setMsgPointer();
		}
		break;

		case ASK_MAKE_CHORD:
		{
			restoreStatusAndMousePointer();
			mixChordSample();
		}
		break;

		case ASK_BOOST_ALL_SAMPLES:
		{
			restoreStatusAndMousePointer();

			for (i = 0; i < MOD_SAMPLES; i++)
				boostSample(i, true);

			if (editor.ui.samplerScreenShown)
				redrawSample();

			updateWindowTitle(MOD_IS_MODIFIED);
		}
		break;

		case ASK_FILTER_ALL_SAMPLES:
		{
			restoreStatusAndMousePointer();

			for (i = 0; i < MOD_SAMPLES; i++)
				filterSample(i, true);

			if (editor.ui.samplerScreenShown)
				redrawSample();

			updateWindowTitle(MOD_IS_MODIFIED);
		}
		break;

		case ASK_UPSAMPLE:
		{
			restoreStatusAndMousePointer();

			s = &modEntry->samples[editor.currSample];

			tmpSmpBuffer = (int8_t *)malloc(s->length);
			if (tmpSmpBuffer == NULL)
			{
				statusOutOfMemory();
				return;
			}

			newLength = (s->length / 2) & 0xFFFE;
			if (newLength < 2)
				return;

			turnOffVoices();

			memcpy(tmpSmpBuffer, &modEntry->sampleData[s->offset], s->length);

			// upsample
			for (j = 0; j < newLength; j++)
				modEntry->sampleData[s->offset + j] = tmpSmpBuffer[j * 2];

			if (newLength < MAX_SAMPLE_LEN)
				memset(&modEntry->sampleData[s->offset + newLength], 0, MAX_SAMPLE_LEN - newLength);

			free(tmpSmpBuffer);

			s->length = newLength;
			s->loopStart = (s->loopStart / 2) & 0xFFFE;
			s->loopLength = (s->loopLength / 2) & 0xFFFE;

			if (s->loopLength < 2)
			{
				s->loopStart = 0;
				s->loopLength = 2;
			}

			fixSampleBeep(s);
			updateCurrSample();

			editor.ui.updateSongSize = true;
			updateWindowTitle(MOD_IS_MODIFIED);
		}
		break;

		case ASK_DOWNSAMPLE:
		{
			restoreStatusAndMousePointer();

			s = &modEntry->samples[editor.currSample];

			tmpSmpBuffer = (int8_t *)malloc(s->length);
			if (tmpSmpBuffer == NULL)
			{
				statusOutOfMemory();
				return;
			}

			newLength = s->length * 2;
			if (newLength > MAX_SAMPLE_LEN)
				newLength = MAX_SAMPLE_LEN;

			turnOffVoices();

			memcpy(tmpSmpBuffer, &modEntry->sampleData[s->offset], s->length);

			// downsample
			for (j = 0; j < newLength; j++)
				modEntry->sampleData[s->offset+j] = tmpSmpBuffer[j >> 1];

			if (newLength < MAX_SAMPLE_LEN)
				memset(&modEntry->sampleData[s->offset+newLength], 0, MAX_SAMPLE_LEN - newLength);

			free(tmpSmpBuffer);

			s->length = newLength;

			if (s->loopLength > 2)
			{
				loopStart = s->loopStart * 2;
				loopLength = s->loopLength * 2;

				if (loopStart+loopLength > s->length)
				{
					loopStart = 0;
					loopLength = 2;
				}

				s->loopStart = (uint16_t)loopStart;
				s->loopLength = (uint16_t)loopLength;
			}

			fixSampleBeep(s);
			updateCurrSample();

			editor.ui.updateSongSize = true;
			updateWindowTitle(MOD_IS_MODIFIED);
		}
		break;

		case ASK_KILL_SAMPLE:
		{
			restoreStatusAndMousePointer();

			turnOffVoices();
			s = &modEntry->samples[editor.currSample];

			s->fineTune = 0;
			s->volume = 0;
			s->length = 0;
			s->loopStart = 0;
			s->loopLength = 2;

			memset(s->text, 0, sizeof (s->text));
			memset(&modEntry->sampleData[(editor.currSample * MAX_SAMPLE_LEN)], 0, MAX_SAMPLE_LEN);

			editor.samplePos = 0;
			updateCurrSample();

			editor.ui.updateSongSize = true;
			updateWindowTitle(MOD_IS_MODIFIED);
		}
		break;

		case ASK_RESAMPLE:
		{
			restoreStatusAndMousePointer();
			samplerResample();
		}
		break;

		case ASK_LOAD_DOWNSAMPLE:
		{
			// for WAV and AIFF sample loader
			restoreStatusAndMousePointer();
			extLoadWAVOrAIFFSampleCallback(DO_DOWNSAMPLE);
		}
		break;

		case ASK_MOD2WAV_OVERWRITE:
		{
			memset(fileName, 0, sizeof (fileName));

			if (modEntry->head.moduleTitle[0] != '\0')
			{
				for (i = 0; i < 20; i++)
				{
					fileName[i] = (char)tolower(modEntry->head.moduleTitle[i]);
					if (fileName[i] == '\0') break;
					sanitizeFilenameChar(&fileName[i]);
				}

				strcat(fileName, ".wav");
			}
			else
			{
				strcpy(fileName, "untitled.wav");
			}

			renderToWav(fileName, DONT_CHECK_IF_FILE_EXIST);
		}
		break;

		case ASK_MOD2WAV:
		{
			memset(fileName, 0, sizeof (fileName));

			if (modEntry->head.moduleTitle[0] != '\0')
			{
				for (i = 0; i < 20; i++)
				{
					fileName[i] = (char)(tolower(modEntry->head.moduleTitle[i]));
					if (fileName[i] == '\0') break;
					sanitizeFilenameChar(&fileName[i]);
				}

				strcat(fileName, ".wav");
			}
			else
			{
				strcpy(fileName, "untitled.wav");
			}

			renderToWav(fileName, CHECK_IF_FILE_EXIST);
		}
		break;

		case ASK_QUIT:
		{
			restoreStatusAndMousePointer();
			editor.ui.throwExit = true;
		}
		break;

		case ASK_SAVE_SAMPLE:
		{
			restoreStatusAndMousePointer();
			saveSample(CHECK_IF_FILE_EXIST, DONT_GIVE_NEW_FILENAME);
		}
		break;

		case ASK_SAVESMP_OVERWRITE:
		{
			restoreStatusAndMousePointer();
			saveSample(DONT_CHECK_IF_FILE_EXIST, DONT_GIVE_NEW_FILENAME);
		}
		break;

		case ASK_SAVE_MODULE:
		{
			restoreStatusAndMousePointer();
			saveModule(CHECK_IF_FILE_EXIST, DONT_GIVE_NEW_FILENAME);
		}
		break;

		case ASK_SAVEMOD_OVERWRITE:
		{
			restoreStatusAndMousePointer();
			saveModule(DONT_CHECK_IF_FILE_EXIST, DONT_GIVE_NEW_FILENAME);
		}
		break;

		default: break;
	}

	removeAskDialog();
}

void createBitmaps(void)
{
	uint8_t r8, g8, b8, r8_2, g8_2, b8_2;
	uint16_t pixel12;
	uint32_t i, j, x, y, pixel24;

	pixel24 = video.palette[PAL_PATCURSOR];
	for (y = 0; y < 14; y++)
	{
		// top two rows have a lighter color
		if (y < 2)
		{
			r8 = R24(pixel24);
			g8 = G24(pixel24);
			b8 = B24(pixel24);

			if (r8 <= 0xFF-0x33)
				r8 += 0x33;
			else
				r8 = 0xFF;

			if (g8 <= 0xFF-0x33)
				g8 += 0x33;
			else
				g8 = 0xFF;

			if (b8 <= 0xFF-0x33)
				b8 += 0x33;
			else
				b8 = 0xFF;

			for (x = 0; x < 11; x++)
				patternCursorBMP[(y * 11) + x] = RGB24(r8, g8, b8);
		}

		// sides (same color)
		if (y >= 2 && y <= 12)
		{
			patternCursorBMP[(y * 11) + 0] = pixel24;

			for (x = 1; x < 10; x++)
				patternCursorBMP[(y * 11) + x] = video.palette[PAL_COLORKEY];

			patternCursorBMP[(y * 11) + 10] = pixel24;
		}

		// bottom two rows have a darker color
		if (y > 11)
		{
			r8 = R24(pixel24);
			g8 = G24(pixel24);
			b8 = B24(pixel24);

			if (r8 >= 0x33)
				r8 -= 0x33;
			else
				r8 = 0x00;

			if (g8 >= 0x33)
				g8 -= 0x33;
			else
				g8 = 0x00;

			if (b8 >= 0x33)
				b8 -= 0x33;
			else
				b8 = 0x00;

			for (x = 0; x < 11; x++)
				patternCursorBMP[(y * 11) + x] = RGB24(r8, g8, b8);
		}
	}

	// create spectrum analyzer bar graphics
	for (i = 0; i < 36; i++)
		spectrumAnaBMP[i] = RGB12_to_RGB24(analyzerColors[35-i]);

	// create VU-Meter bar graphics
	for (i = 0; i < 48; i++)
	{
		pixel12 = vuMeterColors[47-i];

		r8_2 = r8 = R12_to_R24(pixel12);
		g8_2 = g8 = G12_to_G24(pixel12);
		b8_2 = b8 = B12_to_B24(pixel12);

		// brighter pixels on the left side

		if (r8_2 <= 0xFF-0x33)
			r8_2 += 0x33;
		else
			r8_2 = 0xFF;

		if (g8_2 <= 0xFF-0x33)
			g8_2 += 0x33;
		else
			g8_2 = 0xFF;

		if (b8_2 <= 0xFF-0x33)
			b8_2 += 0x33;
		else
			b8_2 = 0xFF;

		pixel24 = RGB24(r8_2, g8_2, b8_2);

		vuMeterBMP[(i * 10) + 0] = pixel24;
		vuMeterBMP[(i * 10) + 1] = pixel24;

		// main pixels
		for (j = 2; j < 8; j++)
			vuMeterBMP[(i * 10) + j] = RGB24(r8, g8, b8);

		// darker pixels on the right side
		r8_2 = r8;
		g8_2 = g8;
		b8_2 = b8;

		if (r8_2 >= 0x33)
			r8_2 -= 0x33;
		else
			r8_2 = 0x00;

		if (g8_2 >= 0x33)
			g8_2 -= 0x33;
		else
			g8_2 = 0x00;

		if (b8_2 >= 0x33)
			b8_2 -= 0x33;
		else
			b8_2 = 0x00;

		pixel24 = RGB24(r8_2, g8_2, b8_2);

		vuMeterBMP[(i * 10) + 8] = pixel24;
		vuMeterBMP[(i * 10) + 9] = pixel24;
	}

	for (i = 0; i < 30; i++) arrowBMP[i] = video.palette[arrowPaletteBMP[i]];
	for (i = 0; i < 64; i++) samplingPosBMP[i] = samplingPosBMP[i];
	for (i = 0; i < 512; i++) loopPinsBMP[i] = loopPinsBMP[i];
}

void freeBMPs(void)
{
	if (trackerFrameBMP != NULL) free(trackerFrameBMP);
	if (samplerScreenBMP != NULL) free(samplerScreenBMP);
	if (samplerVolumeBMP != NULL) free(samplerVolumeBMP);
	if (samplerFiltersBMP != NULL) free(samplerFiltersBMP);
	if (clearDialogBMP != NULL) free(clearDialogBMP);
	if (diskOpScreenBMP != NULL) free(diskOpScreenBMP);
	if (mod2wavBMP != NULL) free(mod2wavBMP);
	if (posEdBMP != NULL) free(posEdBMP);
	if (spectrumVisualsBMP != NULL) free(spectrumVisualsBMP);
	if (yesNoDialogBMP != NULL) free(yesNoDialogBMP);
	if (bigYesNoDialogBMP != NULL) free(bigYesNoDialogBMP);
	if (pat2SmpDialogBMP != NULL) free(pat2SmpDialogBMP);
	if (editOpScreen1BMP != NULL) free(editOpScreen1BMP);
	if (editOpScreen2BMP != NULL) free(editOpScreen2BMP);
	if (editOpScreen3BMP != NULL) free(editOpScreen3BMP);
	if (editOpScreen4BMP != NULL) free(editOpScreen4BMP);
	if (aboutScreenBMP != NULL) free(aboutScreenBMP);
	if (muteButtonsBMP != NULL) free(muteButtonsBMP);
	if (editOpModeCharsBMP != NULL) free(editOpModeCharsBMP);
	if (arrowBMP != NULL) free(arrowBMP);
}

uint32_t *unpackBMP(const uint8_t *src, uint32_t packedLen)
{
	const uint8_t *packSrc;
	uint8_t *tmpBuffer, *packDst, byteIn;
	int16_t count;
	uint32_t *dst, decodedLength, i;

	// RLE decode
	decodedLength = (src[0] << 24) | (src[1] << 16) | (src[2] << 8) | src[3];

	// 2-bit to 8-bit conversion
	dst = (uint32_t *)malloc((decodedLength * 4) * sizeof (int32_t));
	if (dst == NULL)
		return NULL;

	tmpBuffer = (uint8_t *)malloc(decodedLength + 512); // some margin is needed, the packer is buggy
	if (tmpBuffer == NULL)
	{
		free(dst);
		return NULL;
	}

	packSrc = src + 4;
	packDst = tmpBuffer;

	i = packedLen - 4;
	while (i > 0)
	{
		byteIn = *packSrc++;
		if (byteIn == 0xCC) // compactor code
		{
			count  = *packSrc++;
			byteIn = *packSrc++;

			while (count-- >= 0)
				*packDst++ = byteIn;

			i -= 2;
		}
		else
		{
			*packDst++ = byteIn;
		}

		i--;
	}

	for (i = 0; i < decodedLength; i++)
	{
		byteIn = (tmpBuffer[i] & 0xC0) >> 6;
		assert(byteIn < PALETTE_NUM);
		dst[(i * 4) + 0] = video.palette[byteIn];

		byteIn = (tmpBuffer[i] & 0x30) >> 4;
		assert(byteIn < PALETTE_NUM);
		dst[(i * 4) + 1] = video.palette[byteIn];

		byteIn = (tmpBuffer[i] & 0x0C) >> 2;
		assert(byteIn < PALETTE_NUM);
		dst[(i * 4) + 2] = video.palette[byteIn];

		byteIn = (tmpBuffer[i] & 0x03) >> 0;
		assert(byteIn < PALETTE_NUM);
		dst[(i * 4) + 3] = video.palette[byteIn];
	}

	free(tmpBuffer);
	return dst;
}

bool unpackBMPs(void)
{
	trackerFrameBMP = unpackBMP(trackerFramePackedBMP, sizeof (trackerFramePackedBMP));
	samplerScreenBMP = unpackBMP(samplerScreenPackedBMP, sizeof (samplerScreenPackedBMP));
	samplerVolumeBMP = unpackBMP(samplerVolumePackedBMP, sizeof (samplerVolumePackedBMP));
	samplerFiltersBMP = unpackBMP(samplerFiltersPackedBMP, sizeof (samplerFiltersPackedBMP));
	clearDialogBMP = unpackBMP(clearDialogPackedBMP, sizeof (clearDialogPackedBMP));
	diskOpScreenBMP = unpackBMP(diskOpScreenPackedBMP, sizeof (diskOpScreenPackedBMP));
	mod2wavBMP = unpackBMP(mod2wavPackedBMP, sizeof (mod2wavPackedBMP));
	posEdBMP = unpackBMP(posEdPackedBMP, sizeof (posEdPackedBMP));
	spectrumVisualsBMP = unpackBMP(spectrumVisualsPackedBMP, sizeof (spectrumVisualsPackedBMP));
	yesNoDialogBMP = unpackBMP(yesNoDialogPackedBMP, sizeof (yesNoDialogPackedBMP));
	bigYesNoDialogBMP = unpackBMP(bigYesNoDialogPackedBMP, sizeof (bigYesNoDialogPackedBMP));
	pat2SmpDialogBMP = unpackBMP(pat2SmpDialogPackedBMP, sizeof (pat2SmpDialogPackedBMP));
	editOpScreen1BMP = unpackBMP(editOpScreen1PackedBMP, sizeof (editOpScreen1PackedBMP));
	editOpScreen2BMP = unpackBMP(editOpScreen2PackedBMP, sizeof (editOpScreen2PackedBMP));
	editOpScreen3BMP = unpackBMP(editOpScreen3PackedBMP, sizeof (editOpScreen3PackedBMP));
	editOpScreen4BMP = unpackBMP(editOpScreen4PackedBMP, sizeof (editOpScreen4PackedBMP));
	aboutScreenBMP = unpackBMP(aboutScreenPackedBMP, sizeof (aboutScreenPackedBMP));
	muteButtonsBMP = unpackBMP(muteButtonsPackedBMP, sizeof (muteButtonsPackedBMP));
	editOpModeCharsBMP = unpackBMP(editOpModeCharsPackedBMP, sizeof (editOpModeCharsPackedBMP));

	arrowBMP = (uint32_t *)malloc(30 * sizeof (int32_t)); // different format

	if (trackerFrameBMP    == NULL || samplerScreenBMP   == NULL || samplerVolumeBMP  == NULL ||
		clearDialogBMP     == NULL || diskOpScreenBMP    == NULL || mod2wavBMP        == NULL ||
		posEdBMP           == NULL || spectrumVisualsBMP == NULL || yesNoDialogBMP    == NULL ||
		editOpScreen1BMP   == NULL || editOpScreen2BMP   == NULL || editOpScreen3BMP  == NULL ||
		editOpScreen4BMP   == NULL || aboutScreenBMP     == NULL || muteButtonsBMP    == NULL ||
		editOpModeCharsBMP == NULL || arrowBMP           == NULL || samplerFiltersBMP == NULL ||
		yesNoDialogBMP     == NULL || bigYesNoDialogBMP  == NULL)
	{
		showErrorMsgBox("Out of memory!");
		return false; // BMPs are free'd in cleanUp()
	}

	createBitmaps();
	return true;
}

void videoClose(void)
{
	SDL_DestroyTexture(video.texture);
	SDL_DestroyRenderer(video.renderer);
	SDL_DestroyWindow(video.window);
	free(video.frameBufferUnaligned);
}

void setupSprites(void)
{
	memset(sprites, 0, sizeof (sprites));

	sprites[SPRITE_MOUSE_POINTER].data = mousePointerBMP;
	sprites[SPRITE_MOUSE_POINTER].pixelType = SPRITE_TYPE_PALETTE;
	sprites[SPRITE_MOUSE_POINTER].colorKey = PAL_COLORKEY;
	sprites[SPRITE_MOUSE_POINTER].w = 16;
	sprites[SPRITE_MOUSE_POINTER].h = 16;
	hideSprite(SPRITE_MOUSE_POINTER);

	sprites[SPRITE_PATTERN_CURSOR].data = patternCursorBMP;
	sprites[SPRITE_PATTERN_CURSOR].pixelType = SPRITE_TYPE_RGB;
	sprites[SPRITE_PATTERN_CURSOR].colorKey = video.palette[PAL_COLORKEY];
	sprites[SPRITE_PATTERN_CURSOR].w = 11;
	sprites[SPRITE_PATTERN_CURSOR].h = 14;
	hideSprite(SPRITE_PATTERN_CURSOR);

	sprites[SPRITE_LOOP_PIN_LEFT].data = loopPinsBMP;
	sprites[SPRITE_LOOP_PIN_LEFT].pixelType = SPRITE_TYPE_RGB;
	sprites[SPRITE_LOOP_PIN_LEFT].colorKey = video.palette[PAL_COLORKEY];
	sprites[SPRITE_LOOP_PIN_LEFT].w = 4;
	sprites[SPRITE_LOOP_PIN_LEFT].h = 64;
	hideSprite(SPRITE_LOOP_PIN_LEFT);

	sprites[SPRITE_LOOP_PIN_RIGHT].data = &loopPinsBMP[4 * 64];
	sprites[SPRITE_LOOP_PIN_RIGHT].pixelType = SPRITE_TYPE_RGB;
	sprites[SPRITE_LOOP_PIN_RIGHT].colorKey = video.palette[PAL_COLORKEY];
	sprites[SPRITE_LOOP_PIN_RIGHT].w = 4;
	sprites[SPRITE_LOOP_PIN_RIGHT].h = 64;
	hideSprite(SPRITE_LOOP_PIN_RIGHT);

	sprites[SPRITE_SAMPLING_POS_LINE].data = samplingPosBMP;
	sprites[SPRITE_SAMPLING_POS_LINE].pixelType = SPRITE_TYPE_RGB;
	sprites[SPRITE_SAMPLING_POS_LINE].colorKey = video.palette[PAL_COLORKEY];
	sprites[SPRITE_SAMPLING_POS_LINE].w = 1;
	sprites[SPRITE_SAMPLING_POS_LINE].h = 64;
	hideSprite(SPRITE_SAMPLING_POS_LINE);

	// setup refresh buffer (used to clear sprites after each frame)
	for (uint32_t i = 0; i < SPRITE_NUM; i++)
		sprites[i].refreshBuffer = (uint32_t *)malloc((sprites[i].w * sprites[i].h) * sizeof (int32_t));
}

void freeSprites(void)
{
	for (uint8_t i = 0; i < SPRITE_NUM; i++)
		free(sprites[i].refreshBuffer);
}

void setSpritePos(uint8_t sprite, uint16_t x, uint16_t y)
{
	sprites[sprite].newX = x;
	sprites[sprite].newY = y;
}

void hideSprite(uint8_t sprite)
{
	sprites[sprite].newX = SCREEN_W;
}

void eraseSprites(void)
{
	int32_t sx, sy, x, y, sw, sh, srcPitch, dstPitch;
	const uint32_t *src32;
	uint32_t *dst32;
	sprite_t *s;

	for (int32_t i = SPRITE_NUM-1; i >= 0; i--) // erasing must be done in reverse order
	{
		s = &sprites[i];
		if (s->x >= SCREEN_W || s->y >= SCREEN_H) // sprite is hidden, don't draw nor fill clear buffer
			continue;

		assert(s->refreshBuffer != NULL);

		sw = s->w;
		sh = s->h;
		sx = s->x;
		sy = s->y;

		// if x is negative, adjust variables
		if (sx < 0)
		{
			sw += sx; // subtraction
			sx = 0;
		}

		// if y is negative, adjust variables
		if (sy < 0)
		{
			sh += sy; // subtraction
			sy = 0;
		}

		dst32 = &video.frameBuffer[(sy * SCREEN_W) + sx];
		src32 = s->refreshBuffer;

		// handle x/y clipping
		if (sx+sw >= SCREEN_W) sw = SCREEN_W - sx;
		if (sy+sh >= SCREEN_H) sh = SCREEN_H - sy;

		srcPitch = s->w - sw;
		dstPitch = SCREEN_W - sw;

		for (y = 0; y < sh; y++)
		{
			for (x = 0; x < sw; x++)
				*dst32++ = *src32++;

			src32 += srcPitch;
			dst32 += dstPitch;
		}
	}

	fillFromVuMetersBgBuffer(); // let's put it here even though it's not sprite-based
}

void renderSprites(void)
{
	const uint8_t *src8;
	int32_t sx, sy, x, y, srcPtrBias, sw, sh, srcPitch, dstPitch;
	const uint32_t *src32;
	uint32_t *dst32, *clr32, colorKey;
	sprite_t *s;

	renderVuMeters(); // let's put it here even though it's not sprite-based

	for (int32_t i = 0; i < SPRITE_NUM; i++)
	{
		s = &sprites[i];

		// set new sprite position
		s->x = s->newX;
		s->y = s->newY;

		if (s->x >= SCREEN_W || s->y >= SCREEN_H) // sprite is hidden, don't draw nor fill clear buffer
			continue;

		assert(s->data != NULL && s->refreshBuffer != NULL);

		sw = s->w;
		sh = s->h;
		sx = s->x;
		sy = s->y;
		srcPtrBias = 0;

		// if x is negative, adjust variables
		if (sx < 0)
		{
			sw += sx; // subtraction
			srcPtrBias += -sx;
			sx = 0;
		}

		// if y is negative, adjust variables
		if (sy < 0)
		{
			sh += sy; // subtraction
			srcPtrBias += (-sy * s->w);
			sy = 0;
		}

		dst32 = &video.frameBuffer[(sy * SCREEN_W) + sx];
		clr32 = s->refreshBuffer;

		// handle x/y clipping
		if (sx+sw >= SCREEN_W) sw = SCREEN_W - sx;
		if (sy+sh >= SCREEN_H) sh = SCREEN_H - sy;

		srcPitch = s->w - sw;
		dstPitch = SCREEN_W - sw;

		colorKey = sprites[i].colorKey;
		if (sprites[i].pixelType == SPRITE_TYPE_RGB)
		{
			// 24-bit RGB sprite
			src32 = ((uint32_t *)sprites[i].data) + srcPtrBias;
			for (y = 0; y < sh; y++)
			{
				for (x = 0; x < sw; x++)
				{
					*clr32++ = *dst32; // fill clear buffer
					if (*src32 != colorKey)
						*dst32 = *src32;

					dst32++;
					src32++;
				}

				clr32 += srcPitch;
				src32 += srcPitch;
				dst32 += dstPitch;
			}
		}
		else
		{
			// 8-bit paletted sprite
			src8 = ((uint8_t *)sprites[i].data) + srcPtrBias;
			for (y = 0; y < sh; y++)
			{
				for (x = 0; x < sw; x++)
				{
					*clr32++ = *dst32; // fill clear buffer
					if (*src8 != colorKey)
					{
						assert(*src8 < PALETTE_NUM);
						*dst32 = video.palette[*src8];
					}

					dst32++;
					src8++;
				}

				clr32 += srcPitch;
				src8 += srcPitch;
				dst32 += dstPitch;
			}
		}
	}
}

void flipFrame(void)
{
	renderSprites();
	SDL_UpdateTexture(video.texture, NULL, video.frameBuffer, SCREEN_W * sizeof (int32_t));
	SDL_RenderClear(video.renderer);
	SDL_RenderCopy(video.renderer, video.texture, NULL, NULL);
	SDL_RenderPresent(video.renderer);
	eraseSprites();

	if (!video.vsync60HzPresent)
	{
		waitVBL(); // we have no VSync, do crude thread sleeping to sync to ~60Hz
	}
	else
	{
		uint32_t windowFlags = SDL_GetWindowFlags(video.window);

		/* We have VSync, but it can unexpectedly get inactive in certain scenarios.
		** We have to force thread sleeping (to ~60Hz) if so.
		*/
#ifdef __APPLE__
		// macOS: VSync gets disabled if the window is 100% covered by another window. Let's add a (crude) fix:
		if ((windowFlags & SDL_WINDOW_MINIMIZED) || !(windowFlags & SDL_WINDOW_INPUT_FOCUS))
			waitVBL();
#elif __unix__
		// *NIX: VSync gets disabled in fullscreen mode (at least on some distros/systems). Let's add a fix:
		if ((windowFlags & SDL_WINDOW_MINIMIZED) || video.fullscreen)
			waitVBL();
#else
		if (windowFlags & SDL_WINDOW_MINIMIZED)
			waitVBL();
#endif
	}
}

void updateSpectrumAnalyzer(int8_t vol, int16_t period)
{
	const uint8_t maxHeight = SPECTRUM_BAR_HEIGHT + 1; // +1 because of audio latency - allows full height to be seen
	int32_t scaledVol, scaledNote;

	if (editor.ui.visualizerMode != VISUAL_SPECTRUM || vol <= 0)
		return;

	scaledVol = (vol * 24600L) >> 16; // scaledVol = (vol << 8) / 682

	period = CLAMP(period, 113, 856);

	scaledNote = 856 - period;
	scaledNote *= scaledNote;
	scaledNote = ((int64_t)scaledNote * 171162) >> 32; // scaledNote /= 25093

	// scaledNote now ranges 0..22, no need to clamp

	// increment main spectrum bar
	editor.spectrumVolumes[scaledNote] += scaledVol;
	if (editor.spectrumVolumes[scaledNote] > maxHeight)
		editor.spectrumVolumes[scaledNote] = maxHeight;

	// increment left side of spectrum bar with half volume
	if (scaledNote > 0)
	{
		editor.spectrumVolumes[scaledNote-1] += scaledVol >> 1;
		if (editor.spectrumVolumes[scaledNote-1] > maxHeight)
			editor.spectrumVolumes[scaledNote-1] = maxHeight;
	}

	// increment right side of spectrum bar with half volume
	if (scaledNote < SPECTRUM_BAR_NUM-1)
	{
		editor.spectrumVolumes[scaledNote+1] += scaledVol >> 1;
		if (editor.spectrumVolumes[scaledNote+1] > maxHeight)
			editor.spectrumVolumes[scaledNote+1] = maxHeight;
	}
}

void sinkVisualizerBars(void)
{
	int32_t i;

	// sink stuff @ 50Hz rate

	static uint64_t counter50Hz;
	const uint64_t counter50HzDelta = ((uint64_t)AMIGA_PAL_VBLANK_HZ << 32) / VBLANK_HZ;

	counter50Hz += counter50HzDelta; // 32.32 fixed-point counter
	if (counter50Hz > 0xFFFFFFFF)
	{
		counter50Hz &= 0xFFFFFFFF;

		// sink VU-meters
		for (i = 0; i < AMIGA_VOICES; i++)
		{
			if (editor.vuMeterVolumes[i] > 0)
				editor.vuMeterVolumes[i]--;
		}

		// sink "spectrum analyzer" bars
		for (i = 0; i < SPECTRUM_BAR_NUM; i++)
		{
			if (editor.spectrumVolumes[i] > 0)
				editor.spectrumVolumes[i]--;
		}
	}
}

void updateRenderSizeVars(void)
{
	int32_t di;
#ifdef __APPLE__
	int32_t actualScreenW, actualScreenH;
	double dXUpscale, dYUpscale;
#endif
	float fXScale, fYScale;
	SDL_DisplayMode dm;

	di = SDL_GetWindowDisplayIndex(video.window);
	if (di < 0)
		di = 0; /* return display index 0 (default) on error */

	SDL_GetDesktopDisplayMode(di, &dm);
	video.displayW = dm.w;
	video.displayH = dm.h;

	if (video.fullscreen)
	{
		if (config.fullScreenStretch)
		{
			video.renderW = video.displayW;
			video.renderH = video.displayH;
			video.renderX = 0;
			video.renderY = 0;
		}
		else
		{
			SDL_RenderGetScale(video.renderer, &fXScale, &fYScale);

			video.renderW = (int32_t)(SCREEN_W * fXScale);
			video.renderH = (int32_t)(SCREEN_H * fYScale);

#ifdef __APPLE__
			// retina high-DPI hackery (SDL2 is bad at reporting actual rendering sizes on macOS w/ high-DPI)
			SDL_GL_GetDrawableSize(video.window, &actualScreenW, &actualScreenH);
			SDL_GetDesktopDisplayMode(0, &dm);

			dXUpscale = ((double)actualScreenW / video.displayW);
			dYUpscale = ((double)actualScreenH / video.displayH);

			// downscale back to correct sizes
			if (dXUpscale != 0.0) video.renderW = (int32_t)(video.renderW / dXUpscale);
			if (dYUpscale != 0.0) video.renderH = (int32_t)(video.renderH / dYUpscale);
#endif
			video.renderX = (video.displayW - video.renderW) >> 1;
			video.renderY = (video.displayH - video.renderH) >> 1;
		}
	}
	else
	{
		SDL_GetWindowSize(video.window, &video.renderW, &video.renderH);

		video.renderX = 0;
		video.renderY = 0;
	}

	// for mouse cursor creation
	video.xScale = (int32_t)((video.renderW * (1.0 / SCREEN_W)) + 0.5);
	video.yScale = (int32_t)((video.renderH * (1.0 / SCREEN_H)) + 0.5);
	createMouseCursors();
}

void toggleFullScreen(void)
{
	SDL_DisplayMode dm;

	video.fullscreen ^= 1;
	if (video.fullscreen)
	{
		if (config.fullScreenStretch)
		{
			SDL_GetDesktopDisplayMode(0, &dm);
			SDL_RenderSetLogicalSize(video.renderer, dm.w, dm.h);
		}
		else
		{
			SDL_RenderSetLogicalSize(video.renderer, SCREEN_W, SCREEN_H);
		}

		SDL_SetWindowSize(video.window, SCREEN_W, SCREEN_H);
		SDL_SetWindowFullscreen(video.window, SDL_WINDOW_FULLSCREEN_DESKTOP);
		SDL_SetWindowGrab(video.window, SDL_TRUE);
	}
	else
	{
		SDL_SetWindowFullscreen(video.window, 0);
		SDL_RenderSetLogicalSize(video.renderer, SCREEN_W, SCREEN_H);
		SDL_SetWindowSize(video.window, SCREEN_W * config.videoScaleFactor, SCREEN_H * config.videoScaleFactor);
		SDL_SetWindowPosition(video.window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
		SDL_SetWindowGrab(video.window, SDL_FALSE);
	}

	updateRenderSizeVars();
	updateMouseScaling();

	if (video.fullscreen)
	{
		mouse.setPosX = video.displayW >> 1;
		mouse.setPosY = video.displayH >> 1;
	}
	else
	{
		mouse.setPosX = video.renderW >> 1;
		mouse.setPosY = video.renderH >> 1;
	}

	mouse.setPosFlag = true;
}

bool setupVideo(void)
{
	int32_t screenW, screenH;
	uint32_t rendererFlags;
	SDL_DisplayMode dm;

	screenW = SCREEN_W * config.videoScaleFactor;
	screenH = SCREEN_H * config.videoScaleFactor;

	rendererFlags = 0;

#ifdef _WIN32
#if SDL_PATCHLEVEL >= 4
	SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, "1"); // this is for Windows only
#endif
#endif

#if SDL_PATCHLEVEL >= 5
	SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
#endif

	video.vsync60HzPresent = false;
	if (!config.vsyncOff)
	{
		SDL_GetDesktopDisplayMode(0, &dm);
		if (dm.refresh_rate >= 59 && dm.refresh_rate <= 61)
		{
			video.vsync60HzPresent = true;
			rendererFlags |= SDL_RENDERER_PRESENTVSYNC;
		}
	}

	video.window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED,
		SDL_WINDOWPOS_CENTERED, screenW, screenH,
		SDL_WINDOW_HIDDEN | SDL_WINDOW_ALLOW_HIGHDPI);

	if (video.window == NULL)
	{
		showErrorMsgBox("Couldn't create SDL window:\n%s", SDL_GetError());
		return false;
	}

	video.renderer = SDL_CreateRenderer(video.window, -1, rendererFlags);
	if (video.renderer == NULL)
	{
		if (video.vsync60HzPresent) // try again without vsync flag
		{
			video.vsync60HzPresent = false;
			rendererFlags &= ~SDL_RENDERER_PRESENTVSYNC;
			video.renderer = SDL_CreateRenderer(video.window, -1, rendererFlags);
		}

		if (video.renderer == NULL)
		{
			showErrorMsgBox("Couldn't create SDL renderer:\n%s\n\n" \
			                "Is your GPU (+ driver) too old?", SDL_GetError());
			return false;
		}
	}

	SDL_RenderSetLogicalSize(video.renderer, SCREEN_W, SCREEN_H);

#if SDL_PATCHLEVEL >= 5
	SDL_RenderSetIntegerScale(video.renderer, SDL_TRUE);
#endif

	SDL_SetRenderDrawBlendMode(video.renderer, SDL_BLENDMODE_NONE);

	SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "nearest");

	video.texture = SDL_CreateTexture(video.renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_W, SCREEN_H);
	if (video.texture == NULL)
	{
		showErrorMsgBox("Couldn't create %dx%d GPU texture:\n%s\n\n" \
		                "Is your GPU (+ driver) too old?", SCREEN_W, SCREEN_H, SDL_GetError());
		return false;
	}

	SDL_SetTextureBlendMode(video.texture, SDL_BLENDMODE_NONE);

	// frame buffer used by SDL (for texture)
	video.frameBufferUnaligned = (uint32_t *)MALLOC_PAD(SCREEN_W * SCREEN_H * sizeof (int32_t), 256);
	if (video.frameBufferUnaligned == NULL)
	{
		showErrorMsgBox("Out of memory!");
		return false;
	}

	// we want an aligned pointer
	video.frameBuffer = (uint32_t *)ALIGN_PTR(video.frameBufferUnaligned, 256);

	updateRenderSizeVars();
	updateMouseScaling();

	if (config.hwMouse)
		SDL_ShowCursor(SDL_TRUE);
	else
		SDL_ShowCursor(SDL_FALSE);

	return true;
}
