shithub: pt2-clone

ref: 4205fc20cb6541818c9ffc3ea62e69bce0f7cee6
dir: /src/pt2_visuals.c/

View raw version
// 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_module_loader.h"
#include "pt2_module_saver.h"
#include "pt2_sample_loader.h"
#include "pt2_sample_saver.h"
#include "pt2_pattern_viewer.h"
#include "pt2_sampler.h"
#include "pt2_diskop.h"
#include "pt2_visuals.h"
#include "pt2_scopes.h"
#include "pt2_edit.h"
#include "pt2_pat2smp.h"
#include "pt2_mod2wav.h"
#include "pt2_config.h"
#include "pt2_bmp.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
};


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 statusSampleIsEmpty(void)
{
	displayErrorMsg("SAMPLE IS EMPTY");
}

void statusNotSampleZero(void)
{
	/* This rather confusing error message actually means that
	** you can't load a sample to sample slot #0 (which isn't a
	** real sample slot).
	*/
	displayErrorMsg("NOT SAMPLE 0 !");
}

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;
	dFrac += 0.5;
	if (dFrac > UINT32_MAX)
		dFrac = UINT32_MAX;
	editor.vblankTimeLenFrac = (uint32_t)dFrac;
}

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;
	ui.clearScreenShown = false;
	ui.changingChordNote = false;
	ui.changingSmpResample = false;
	ui.pat2SmpDialogShown = false;
	ui.disablePosEd = false;
	ui.disableVisualizer = false;

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

		displaySample();
	}

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

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

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

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

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

	// render ask dialog

	srcPtr = 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;

	ui.disablePosEd = true;
	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)
{
	ui.askScreenShown = true;
	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 (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 (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 (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 (ui.diskOpScreenShown)
		return;

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

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

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

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

	if (ui.updateCurrSampleFineTune)
	{
		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 (ui.updateCurrSampleNum)
	{
		ui.updateCurrSampleNum = false;
		if (!editor.isWAVRendering)
		{
			printTwoHexBg(80, 47,
				editor.sampleZero ? 0 : ((*editor.currSampleDisp) + 1), video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}

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

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

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

	if (ui.updateCurrSampleReplen)
	{
		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 (ui.updateStatusText)
	{
		ui.updateStatusText = false;

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

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

			editor.blockToPos = song->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, ui.statusMessage, video.palette[PAL_GENTXT]);
		}
	}

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

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

	if (ui.updateTrackerFlags)
	{
		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 (ui.updateSongName)
	{
		ui.updateSongName = false;
		for (x = 0; x < 20; x++)
		{
			tempChar = song->header.name[x];
			if (tempChar == '\0')
				tempChar = '_';

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

	if (ui.updateCurrSampleName)
	{
		ui.updateCurrSampleName = false;
		currSample = &song->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 (ui.updateSongSize)
	{
		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 += song->samples[i].length;

		uint32_t totalPatterns = 0;
		for (i = 0; i < MOD_ORDERS; i++)
		{
			if (song->header.order[i] > totalPatterns)
				totalPatterns = song->header.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 (ui.updateSongTiming)
	{
		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 (!ui.samplerScreenShown)
		setSpritePos(SPRITE_PATTERN_CURSOR, cursorPosTable[cursor.pos], 188);
}

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

	if (!ui.samplerScreenShown)
		return;

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

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

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

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

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

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

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

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

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

		if (ui.updateNormFlag)
		{
			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;
	}

	ui.updateVolFromText = true;
	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]);

	ui.updateLPText = true;
	ui.updateHPText = true;
	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));

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

void updateDiskOp(void)
{
	char tmpChar;

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

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

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

		// draw load mode arrow
		if (diskop.mode == 0)
		{
			charOutBg(147,14, ' ', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]); // clear other box
			charOutBg(147, 3, 0x3, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
		else
		{
			charOutBg(147, 3, ' ', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]); // clear other box
			charOutBg(147,14, 0x3, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}

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

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

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

		// print disk op. path
		for (uint32_t i = 0; i < 26; i++)
		{
			tmpChar = editor.currPath[ui.diskOpPathTextOffset+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 (!ui.posEdScreenShown || !ui.updatePosEd)
		return;

	ui.updatePosEd = false;

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

		posEdPosition = song->currOrder;
		if (posEdPosition > song->header.numOrders-1)
			posEdPosition = song->header.numOrders-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), song->header.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 < song->header.numOrders-1)
			{
				printThreeDecimalsBg(128, 59+(y*6), posEdPosition+(y+1), video.palette[PAL_QADSCP], video.palette[PAL_BACKGRD]);
				printTwoDecimalsBg(160, 59+(y*6), song->header.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 (ui.editTextFlag && 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 (ui.diskOpScreenShown || 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;

	ui.disablePosEd = true;
	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();

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

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

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

	if (ui.samplerScreenShown)
		redrawSample();

	updateSamplePos();
	recalcChordLength();

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

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

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

	if (!ui.editTextFlag)
		return;

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

	if (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

		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 (!ui.editTextFlag)
		return;

	dstPtr = &video.frameBuffer[((ui.lineCurY - 1) * SCREEN_W) + (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 (ui.sampleMarkingPos >= 0)
	{
		samplerSamplePressed(MOUSE_BUTTON_HELD);
		testMouseButtonRelease = true;
	}

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

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

	if (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 (ui.disableVisualizer || ui.diskOpScreenShown ||
		ui.posEdScreenShown  || ui.editOpScreenShown ||
		ui.aboutScreenShown  || ui.askScreenShown    ||
		editor.isWAVRendering)
	{
		return;
	}

	if (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 (!ui.aboutScreenShown || ui.diskOpScreenShown || ui.posEdScreenShown || 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 (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 (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 (ui.editOpScreen == 0)
	{
		textOut(128, 47, "  TRACK      PATTERN  ", video.palette[PAL_GENTXT]);
	}
	else if (ui.editOpScreen == 1)
	{
		textOut(128, 47, "  RECORD     SAMPLES  ", video.palette[PAL_GENTXT]);

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

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

		ui.updateLengthText = true;
		ui.updateNote1Text = true;
		ui.updateNote2Text = true;
		ui.updateNote3Text = true;
		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 (!ui.updateMod2WavDialog)
		return;

	ui.updateMod2WavDialog = false;

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

			resetSong();
			pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);

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

			editor.isWAVRendering = false;
			modSetTempo(song->currBPM); // update BPM with normal audio output rate
			displayMainScreen();
		}
		else
		{
			// render progress bar

			percent = (uint8_t)((song->rowsCounter * 100) / song->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 (!ui.editOpScreenShown || ui.posEdScreenShown || ui.diskOpScreenShown)
		return;

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

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

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

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

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

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

		if (ui.updateToText)
		{
			ui.updateToText = false;
			printTwoHexBg(264, 91, *editor.sampleToDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}
	else if (ui.editOpScreen == 2)
	{
		if (ui.updateMixText)
		{
			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 (ui.updatePosText)
		{
			ui.updatePosText = false;
			printFourHexBg(248, 58, *editor.samplePosDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (ui.updateModText)
		{
			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 (ui.updateVolText)
		{
			ui.updateVolText = false;
			printThreeDecimalsBg(248, 91, *editor.sampleVolDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}
	else if (ui.editOpScreen == 3)
	{
		if (ui.updateLengthText)
		{
			ui.updateLengthText = false;

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

			if (song->samples[editor.currSample].loopLength > 2 || song->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 (ui.updateNote1Text)
		{
			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[2+editor.note1] : noteNames1[2+editor.note1],
					video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (ui.updateNote2Text)
		{
			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[2+editor.note2] : noteNames1[2+editor.note2],
					video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}

		if (ui.updateNote3Text)
		{
			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[2+editor.note3] : noteNames1[2+editor.note3],
					video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
			
		if (ui.updateNote4Text)
		{
			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[2+editor.note4] : noteNames1[2+editor.note4],
					video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
		}
	}
}

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

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

	ui.updateCurrSampleName = true;

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

	if (ui.samplerScreenShown)
	{
		if (!ui.diskOpScreenShown)
			memcpy(video.frameBuffer, trackerFrameBMP, 320 * 121 * sizeof (int32_t));
	}
	else
	{
		if (!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));

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

	if (ui.diskOpScreenShown)
	{
		renderDiskOpScreen();
	}
	else
	{
		ui.updateSongPos = true;
		ui.updateSongPattern = true;
		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 (ui.posEdScreenShown)
		{
			renderPosEdScreen();
			ui.updatePosEd = true;
		}
		else
		{
			if (ui.editOpScreenShown)
			{
				renderEditOpScreen();
			}
			else
			{
				if (ui.aboutScreenShown)
				{
					renderAboutScreen();
				}
				else
				{
					     if (ui.visualizerMode == VISUAL_QUADRASCOPE) renderQuadrascopeBg();
					else if (ui.visualizerMode == VISUAL_SPECTRUM) renderSpectrumAnalyzerBg();
				}
			}

			renderMuteButtons();
		}
	}
}

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

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

	switch (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 oldSample;
	uint32_t i;
	moduleSample_t *s;

	switch (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();
			doPat2Smp();
		}
		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 (song->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 (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 (ui.samplerScreenShown)
				redrawSample();

			updateWindowTitle(MOD_IS_MODIFIED);
		}
		break;

		case ASK_UPSAMPLE:
		{
			restoreStatusAndMousePointer();
			upSample();
		}
		break;

		case ASK_DOWNSAMPLE:
		{
			restoreStatusAndMousePointer();
			downSample();
		}
		break;

		case ASK_KILL_SAMPLE:
		{
			restoreStatusAndMousePointer();

			if (editor.sampleZero)
			{
				statusNotSampleZero();
				break;
			}

			turnOffVoices();
			s = &song->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(&song->sampleData[(editor.currSample * MAX_SAMPLE_LEN)], 0, MAX_SAMPLE_LEN);

			editor.samplePos = 0;
			updateCurrSample();

			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 (song->header.name[0] != '\0')
			{
				for (i = 0; i < 20; i++)
				{
					fileName[i] = (char)tolower(song->header.name[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 (song->header.name[0] != '\0')
			{
				for (i = 0; i < 20; i++)
				{
					fileName[i] = (char)(tolower(song->header.name[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();
			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 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 (int32_t i = 0; i < SPRITE_NUM; i++)
		free(sprites[i].refreshBuffer);
}

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

void hideSprite(int32_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
	int8_t scaledVol;
	int32_t scaledNote;

	if (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;
}