// Copyright (c) 2012- PPSSPP Project.

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License 2.0 for more details.

// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/

// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.

#include <vector>
#include <map>
#include <cmath>
#include <algorithm>

// TODO: Move this somewhere else, cleanup.
#ifndef _WIN32
#include <unistd.h>
#include <sys/time.h>
#endif

// TODO: Move the relevant parts into common. Don't want the core
// to be dependent on "native", I think. Or maybe should get rid of common
// and move everything into native...
#include "base/logging.h"
#include "base/timeutil.h"
#include "i18n/i18n.h"
#include "profiler/profiler.h"

#include "gfx_es2/gpu_features.h"

#include "Common/ChunkFile.h"
#include "Core/Config.h"
#include "Core/CoreTiming.h"
#include "Core/CoreParameter.h"
#include "Core/Host.h"
#include "Core/Reporting.h"
#include "Core/System.h"
#include "Core/HLE/HLE.h"
#include "Core/HLE/FunctionWrappers.h"
#include "Core/HLE/sceDisplay.h"
#include "Core/HLE/sceKernel.h"
#include "Core/HLE/sceKernelThread.h"
#include "Core/HLE/sceKernelInterrupt.h"

#include "GPU/GPU.h"
#include "GPU/GPUState.h"
#include "GPU/GPUInterface.h"
#include "GPU/Common/FramebufferCommon.h"
#include "GPU/Common/PostShader.h"
#include "GPU/Debugger/Record.h"

struct FrameBufferState {
	u32 topaddr;
	GEBufferFormat fmt;
	int stride;
};

struct WaitVBlankInfo {
	WaitVBlankInfo(u32 tid) : threadID(tid), vcountUnblock(1) {}
	WaitVBlankInfo(u32 tid, int vcount) : threadID(tid), vcountUnblock(vcount) {}
	SceUID threadID;
	// Number of vcounts to block for.
	int vcountUnblock;

	void DoState(PointerWrap &p) {
		auto s = p.Section("WaitVBlankInfo", 1);
		if (!s)
			return;

		p.Do(threadID);
		p.Do(vcountUnblock);
	}
};

// STATE BEGIN
static FrameBufferState framebuf;
static FrameBufferState latchedFramebuf;
static bool framebufIsLatched;

static int enterVblankEvent = -1;
static int leaveVblankEvent = -1;
static int afterFlipEvent = -1;
static int lagSyncEvent = -1;

static double lastLagSync = 0.0;
static bool lagSyncScheduled = false;

// hCount is computed now.
static int vCount;
// The "AccumulatedHcount" can be adjusted, this is the base.
static u32 hCountBase;
static int isVblank;
static int numSkippedFrames;
static bool hasSetMode;
static int resumeMode;
static int holdMode;
static int brightnessLevel;
static int mode;
static int width;
static int height;
static bool wasPaused;
static bool flippedThisFrame;

// 1.001f to compensate for the classic 59.94 NTSC framerate that the PSP seems to have.
static const double timePerVblank = 1.001f / 60.0f;

// Don't include this in the state, time increases regardless of state.
static double curFrameTime;
static double lastFrameTime;
static double nextFrameTime;
static int numVBlanks;
static int numVBlanksSinceFlip;

static u64 frameStartTicks;
const int hCountPerVblank = 286;

const int PSP_DISPLAY_MODE_LCD = 0;

std::vector<WaitVBlankInfo> vblankWaitingThreads;
// Key is the callback id it was for, or if no callback, the thread id.
// Value is the goal vcount number (in case the callback takes >= 1 vcount to return.)
std::map<SceUID, int> vblankPausedWaits;

// STATE END

// Called when vblank happens (like an internal interrupt.)  Not part of state, should be static.
std::vector<VblankCallback> vblankListeners;

// The vblank period is 731.5 us (0.7315 ms)
const double vblankMs = 0.7315;
// These are guesses based on tests.
const double vsyncStartMs = 0.5925;
const double vsyncEndMs = 0.7265;
const double frameMs = 1001.0 / 60.0;

enum {
	PSP_DISPLAY_SETBUF_IMMEDIATE = 0,
	PSP_DISPLAY_SETBUF_NEXTFRAME = 1
};

static int lastFpsFrame = 0;
static double lastFpsTime = 0.0;
static double fps = 0.0;
static double fpsHistory[120];
static int fpsHistorySize = (int)ARRAY_SIZE(fpsHistory);
static int fpsHistoryPos = 0;
static int fpsHistoryValid = 0;
static double monitorFpsUntil = 0.0;
static int lastNumFlips = 0;
static float flips = 0.0f;
static int actualFlips = 0;  // taking frameskip into account
static int lastActualFlips = 0;
static float actualFps = 0;
// For the "max 60 fps" setting.
static int lastFlipsTooFrequent = 0;
static u64 lastFlipCycles = 0;
static u64 nextFlipCycles = 0;

void hleEnterVblank(u64 userdata, int cyclesLate);
void hleLeaveVblank(u64 userdata, int cyclesLate);
void hleAfterFlip(u64 userdata, int cyclesLate);
void hleLagSync(u64 userdata, int cyclesLate);

void __DisplayVblankBeginCallback(SceUID threadID, SceUID prevCallbackId);
void __DisplayVblankEndCallback(SceUID threadID, SceUID prevCallbackId);
int __DisplayGetFlipCount() { return actualFlips; }
int __DisplayGetVCount() { return vCount; }
int __DisplayGetNumVblanks() { return numVBlanks; }

void __DisplayFlip(int cyclesLate);

static void ScheduleLagSync(int over = 0) {
	lagSyncScheduled = g_Config.bForceLagSync;
	if (lagSyncScheduled) {
		// Reset over if it became too high, such as after pausing or initial loading.
		// There's no real sense in it being more than 1/60th of a second.
		if (over > 1000000 / 60) {
			over = 0;
		}
		CoreTiming::ScheduleEvent(usToCycles(1000 + over), lagSyncEvent, 0);
		lastLagSync = real_time_now();
	}
}

void __DisplayInit() {
	hasSetMode = false;
	mode = 0;
	resumeMode = 0;
	holdMode = 0;
	brightnessLevel = 84;
	width = 480;
	height = 272;
	numSkippedFrames = 0;
	numVBlanks = 0;
	numVBlanksSinceFlip = 0;
	flippedThisFrame = false;
	framebufIsLatched = false;
	framebuf.topaddr = 0x04000000;
	framebuf.fmt = GE_FORMAT_8888;
	framebuf.stride = 512;
	memcpy(&latchedFramebuf, &framebuf, sizeof(latchedFramebuf));
	lastFlipsTooFrequent = 0;
	lastFlipCycles = 0;
	nextFlipCycles = 0;
	wasPaused = false;

	enterVblankEvent = CoreTiming::RegisterEvent("EnterVBlank", &hleEnterVblank);
	leaveVblankEvent = CoreTiming::RegisterEvent("LeaveVBlank", &hleLeaveVblank);
	afterFlipEvent = CoreTiming::RegisterEvent("AfterFlip", &hleAfterFlip);

	lagSyncEvent = CoreTiming::RegisterEvent("LagSync", &hleLagSync);
	ScheduleLagSync();

	CoreTiming::ScheduleEvent(msToCycles(frameMs - vblankMs), enterVblankEvent, 0);
	isVblank = 0;
	frameStartTicks = 0;
	vCount = 0;
	hCountBase = 0;
	curFrameTime = 0.0;
	nextFrameTime = 0.0;
	lastFrameTime = 0.0;

	flips = 0;
	fps = 0.0;
	actualFlips = 0;
	lastActualFlips = 0;
	lastNumFlips = 0;
	fpsHistoryValid = 0;
	fpsHistoryPos = 0;

	__KernelRegisterWaitTypeFuncs(WAITTYPE_VBLANK, __DisplayVblankBeginCallback, __DisplayVblankEndCallback);
}

struct GPUStatistics_v0 {
	int firstInts[11];
	double msProcessingDisplayLists;
	int moreInts[15];
};

void __DisplayDoState(PointerWrap &p) {
	auto s = p.Section("sceDisplay", 1, 7);
	if (!s)
		return;

	p.Do(framebuf);
	p.Do(latchedFramebuf);
	p.Do(framebufIsLatched);
	p.Do(frameStartTicks);
	p.Do(vCount);
	if (s <= 2) {
		double oldHCountBase;
		p.Do(oldHCountBase);
		hCountBase = (int) oldHCountBase;
	} else {
		p.Do(hCountBase);
	}
	p.Do(isVblank);
	p.Do(hasSetMode);
	p.Do(mode);
	p.Do(resumeMode);
	p.Do(holdMode);
	if (s >= 4) {
		p.Do(brightnessLevel);
	}
	p.Do(width);
	p.Do(height);
	WaitVBlankInfo wvi(0);
	p.Do(vblankWaitingThreads, wvi);
	p.Do(vblankPausedWaits);

	p.Do(enterVblankEvent);
	CoreTiming::RestoreRegisterEvent(enterVblankEvent, "EnterVBlank", &hleEnterVblank);
	p.Do(leaveVblankEvent);
	CoreTiming::RestoreRegisterEvent(leaveVblankEvent, "LeaveVBlank", &hleLeaveVblank);
	p.Do(afterFlipEvent);
	CoreTiming::RestoreRegisterEvent(afterFlipEvent, "AfterFlip", &hleAfterFlip);

	if (s >= 5) {
		p.Do(lagSyncEvent);
		p.Do(lagSyncScheduled);
		CoreTiming::RestoreRegisterEvent(lagSyncEvent, "LagSync", &hleLagSync);
		lastLagSync = real_time_now();
		if (lagSyncScheduled != g_Config.bForceLagSync) {
			ScheduleLagSync();
		}
	} else {
		lagSyncEvent = CoreTiming::RegisterEvent("LagSync", &hleLagSync);
		ScheduleLagSync();
	}

	p.Do(gstate);

	// TODO: GPU stuff is really not the responsibility of sceDisplay.
	// Display just displays the buffers the GPU has drawn, they are really completely distinct.
	// Maybe a bit tricky to move at this point, though...

	gstate_c.DoState(p);
	if (s < 2) {
		// This shouldn't have been savestated anyway, but it was.
		// It's unlikely to overlap with the first value in gpuStats.
		p.ExpectVoid(&gl_extensions.gpuVendor, sizeof(gl_extensions.gpuVendor));
	}
	if (s < 6) {
		GPUStatistics_v0 oldStats;
		p.Do(oldStats);
	}

	if (s < 7) {
		u64 now = CoreTiming::GetTicks();
		lastFlipCycles = now;
		nextFlipCycles = now;
	} else {
		p.Do(lastFlipCycles);
		p.Do(nextFlipCycles);
	}

	gpu->DoState(p);

	if (p.mode == p.MODE_READ) {
		gpu->ReapplyGfxState();

		if (hasSetMode) {
			gpu->InitClear();
		}
		gpu->SetDisplayFramebuffer(framebuf.topaddr, framebuf.stride, framebuf.fmt);
	}
}

void __DisplayShutdown() {
	vblankListeners.clear();
	vblankWaitingThreads.clear();
}

void __DisplayListenVblank(VblankCallback callback) {
	vblankListeners.push_back(callback);
}

static void __DisplayFireVblank() {
	for (std::vector<VblankCallback>::iterator iter = vblankListeners.begin(), end = vblankListeners.end(); iter != end; ++iter) {
		VblankCallback cb = *iter;
		cb();
	}
}

void __DisplayVblankBeginCallback(SceUID threadID, SceUID prevCallbackId) {
	SceUID pauseKey = prevCallbackId == 0 ? threadID : prevCallbackId;

	// This means two callbacks in a row.  PSP crashes if the same callback waits inside itself (may need more testing.)
	// TODO: Handle this better?
	if (vblankPausedWaits.find(pauseKey) != vblankPausedWaits.end()) {
		return;
	}

	WaitVBlankInfo waitData(0);
	for (size_t i = 0; i < vblankWaitingThreads.size(); i++) {
		WaitVBlankInfo *t = &vblankWaitingThreads[i];
		if (t->threadID == threadID) {
			waitData = *t;
			vblankWaitingThreads.erase(vblankWaitingThreads.begin() + i);
			break;
		}
	}

	if (waitData.threadID != threadID) {
		WARN_LOG_REPORT(SCEDISPLAY, "sceDisplayWaitVblankCB: could not find waiting thread info.");
		return;
	}

	vblankPausedWaits[pauseKey] = vCount + waitData.vcountUnblock;
	DEBUG_LOG(SCEDISPLAY, "sceDisplayWaitVblankCB: Suspending vblank wait for callback");
}

void __DisplayVblankEndCallback(SceUID threadID, SceUID prevCallbackId) {
	SceUID pauseKey = prevCallbackId == 0 ? threadID : prevCallbackId;

	// Probably should not be possible.
	if (vblankPausedWaits.find(pauseKey) == vblankPausedWaits.end()) {
		__KernelResumeThreadFromWait(threadID, 0);
		return;
	}

	int vcountUnblock = vblankPausedWaits[pauseKey];
	vblankPausedWaits.erase(pauseKey);
	if (vcountUnblock <= vCount) {
		__KernelResumeThreadFromWait(threadID, 0);
		return;
	}

	// Still have to wait a bit longer.
	vblankWaitingThreads.push_back(WaitVBlankInfo(__KernelGetCurThread(), vcountUnblock - vCount));
	DEBUG_LOG(SCEDISPLAY, "sceDisplayWaitVblankCB: Resuming vblank wait from callback");
}

// TODO: Also average actualFps
void __DisplayGetFPS(float *out_vps, float *out_fps, float *out_actual_fps) {
	*out_vps = fps;
	*out_fps = flips;
	*out_actual_fps = actualFps;
}

void __DisplayGetVPS(float *out_vps) {
	*out_vps = fps;
}

void __DisplayGetAveragedFPS(float *out_vps, float *out_fps) {
	float avg = 0.0;
	if (fpsHistoryValid > 0) {
		for (int i = 0; i < fpsHistoryValid; ++i) {
			avg += fpsHistory[i];
		}
		avg /= (double) fpsHistoryValid;
	}

	*out_vps = *out_fps = avg;
}

static bool IsRunningSlow() {
	// Allow for some startup turbulence for 8 seconds before assuming things are bad.
	if (fpsHistoryValid >= 8) {
		// Look at only the last 15 samples (starting at the 14th sample behind current.)
		int rangeStart = fpsHistoryPos - std::min(fpsHistoryValid, 14);

		double best = 0.0;
		for (int i = rangeStart; i <= fpsHistoryPos; ++i) {
			// rangeStart may have been negative if near a wrap around.
			int index = (fpsHistorySize + i) % fpsHistorySize;
			best = std::max(fpsHistory[index], best);
		}

		// Note that SYSPROP_DISPLAY_REFRESH_RATE is multiplied by 1000.
		return best < System_GetPropertyInt(SYSPROP_DISPLAY_REFRESH_RATE) * (1.0 / 1001.0);
	}

	return false;
}

static void CalculateFPS() {
	time_update();
	double now = time_now_d();

	if (now >= lastFpsTime + 1.0) {
		double frames = (numVBlanks - lastFpsFrame);
		actualFps = (actualFlips - lastActualFlips);

		fps = frames / (now - lastFpsTime);
		flips = 60.0 * (double) (gpuStats.numFlips - lastNumFlips) / frames;

		lastFpsFrame = numVBlanks;
		lastNumFlips = gpuStats.numFlips;
		lastActualFlips = actualFlips;
		lastFpsTime = now;

		fpsHistory[fpsHistoryPos++] = fps;
		fpsHistoryPos = fpsHistoryPos % fpsHistorySize;
		if (fpsHistoryValid < fpsHistorySize) {
			++fpsHistoryValid;
		}
	}
}

void __DisplayGetDebugStats(char *stats, size_t bufsize) {
	char statbuf[4096];
	gpu->GetStats(statbuf, sizeof(statbuf));

	snprintf(stats, bufsize,
		"Kernel processing time: %0.2f ms\n"
		"Slowest syscall: %s : %0.2f ms\n"
		"Most active syscall: %s : %0.2f ms\n%s",
		kernelStats.msInSyscalls * 1000.0f,
		kernelStats.slowestSyscallName ? kernelStats.slowestSyscallName : "(none)",
		kernelStats.slowestSyscallTime * 1000.0f,
		kernelStats.summedSlowestSyscallName ? kernelStats.summedSlowestSyscallName : "(none)",
		kernelStats.summedSlowestSyscallTime * 1000.0f,
		statbuf);
}



void __DisplaySetWasPaused() {
	wasPaused = true;
}

static bool FrameTimingThrottled() {
	if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM1 && g_Config.iFpsLimit1 == 0) {
		return false;
	}
	if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM2 && g_Config.iFpsLimit2 == 0) {
		return false;
	}
	return !PSP_CoreParameter().unthrottle;
}

static void DoFrameDropLogging(float scaledTimestep) {
	if (lastFrameTime != 0.0 && !wasPaused && lastFrameTime + scaledTimestep < curFrameTime) {
		const double actualTimestep = curFrameTime - lastFrameTime;

		char stats[4096];
		__DisplayGetDebugStats(stats, sizeof(stats));
		NOTICE_LOG(SCEDISPLAY, "Dropping frames - budget = %.2fms / %.1ffps, actual = %.2fms (+%.2fms) / %.1ffps\n%s", scaledTimestep * 1000.0, 1.0 / scaledTimestep, actualTimestep * 1000.0, (actualTimestep - scaledTimestep) * 1000.0, 1.0 / actualTimestep, stats);
	}
}

static int CalculateFrameSkip() {
	int frameSkipNum;
	if (g_Config.iFrameSkipType == 1) { 
		// Calculate the frames to skip dynamically using the set percentage of the current fps
		frameSkipNum = ceil( flips * (static_cast<double>(g_Config.iFrameSkip) / 100.00) ); 
	} else { 
		// Use the set number of frames to skip
		frameSkipNum = g_Config.iFrameSkip; 
	}
	return frameSkipNum;
}

// Let's collect all the throttling and frameskipping logic here.
static void DoFrameTiming(bool &throttle, bool &skipFrame, float timestep) {
	PROFILE_THIS_SCOPE("timing");
	FPSLimit fpsLimiter = PSP_CoreParameter().fpsLimit;
	throttle = FrameTimingThrottled();
	skipFrame = false;

	// Check if the frameskipping code should be enabled. If neither throttling or frameskipping is on,
	// we have nothing to do here.
	bool doFrameSkip = g_Config.iFrameSkip != 0;

	if (!throttle && g_Config.bFrameSkipUnthrottle) {
		doFrameSkip = true;
		skipFrame = true;
		if (numSkippedFrames >= 7) {
			skipFrame = false;
		}
		return;
	}

	if (!throttle && !doFrameSkip)
		return;

	time_update();

	float scaledTimestep = timestep;
	if (fpsLimiter == FPSLimit::CUSTOM1 && g_Config.iFpsLimit1 > 0) {
		scaledTimestep *= 60.0f / g_Config.iFpsLimit1;
	} else if (fpsLimiter == FPSLimit::CUSTOM2 && g_Config.iFpsLimit2 > 0) {
		scaledTimestep *= 60.0f / g_Config.iFpsLimit2;
	}

	if (lastFrameTime == 0.0 || wasPaused) {
		nextFrameTime = time_now_d() + scaledTimestep;
	} else {
		// Advance lastFrameTime by a constant amount each frame,
		// but don't let it get too far behind as things can get very jumpy.
		const double maxFallBehindFrames = 5.5;

		nextFrameTime = std::max(lastFrameTime + scaledTimestep, time_now_d() - maxFallBehindFrames * scaledTimestep);
	}
	curFrameTime = time_now_d();

	if (g_Config.bLogFrameDrops) {
		DoFrameDropLogging(scaledTimestep);
	}

	// Auto-frameskip automatically if speed limit is set differently than the default.
	bool useAutoFrameskip = g_Config.bAutoFrameSkip && g_Config.iRenderingMode != FB_NON_BUFFERED_MODE;
	bool forceFrameskip = (fpsLimiter == FPSLimit::CUSTOM1 && g_Config.iFpsLimit1 > 60) || (fpsLimiter == FPSLimit::CUSTOM2 && g_Config.iFpsLimit2 > 60);
	int frameSkipNum = CalculateFrameSkip();
	if (g_Config.bAutoFrameSkip || forceFrameskip) {
		// autoframeskip
		// Argh, we are falling behind! Let's skip a frame and see if we catch up.
		if (curFrameTime > nextFrameTime && doFrameSkip) {
			skipFrame = true;
		}
	} else if (frameSkipNum >= 1) {
		// fixed frameskip
		if (numSkippedFrames >= frameSkipNum)
			skipFrame = false;
		else
			skipFrame = true;
	}

	if (curFrameTime < nextFrameTime && throttle) {
		// If time gap is huge just jump (somebody unthrottled)
		if (nextFrameTime - curFrameTime > 2*scaledTimestep) {
			nextFrameTime = curFrameTime;
		} else {
			// Wait until we've caught up.
			while (time_now_d() < nextFrameTime) {
#ifdef _WIN32
				sleep_ms(1); // Sleep for 1ms on this thread
#else
				const double left = nextFrameTime - curFrameTime;
				usleep((long)(left * 1000000));
#endif
				time_update();
			}
		}
		curFrameTime = time_now_d();
	}

	lastFrameTime = nextFrameTime;
	wasPaused = false;
}

static void DoFrameIdleTiming() {
	PROFILE_THIS_SCOPE("timing");
	if (!FrameTimingThrottled() || !g_Config.bEnableSound || wasPaused) {
		return;
	}

	time_update();

	double dist = time_now_d() - lastFrameTime;
	// Ignore if the distance is just crazy.  May mean wrap or pause.
	if (dist < 0.0 || dist >= 15 * timePerVblank) {
		return;
	}

	float scaledVblank = timePerVblank;
	if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM1 && g_Config.iFpsLimit1 > 0) {
		// 0 is handled in FrameTimingThrottled().
		scaledVblank *= 60.0f / g_Config.iFpsLimit1;
	} else if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM2 && g_Config.iFpsLimit2 > 0) {
		scaledVblank *= 60.0f / g_Config.iFpsLimit2;
	}

	// If we have over at least a vblank of spare time, maintain at least 30fps in delay.
	// This prevents fast forward during loading screens.
	// Give a little extra wiggle room in case the next vblank does more work.
	const double goal = lastFrameTime + (numVBlanksSinceFlip - 1) * scaledVblank - 0.001;
	if (numVBlanksSinceFlip >= 2 && time_now_d() < goal) {
		while (time_now_d() < goal) {
#ifdef _WIN32
			sleep_ms(1);
#else
			const double left = goal - time_now_d();
			usleep((long)(left * 1000000));
#endif
			time_update();
		}
	}
}


void hleEnterVblank(u64 userdata, int cyclesLate) {
	int vbCount = userdata;

	VERBOSE_LOG(SCEDISPLAY, "Enter VBlank %i", vbCount);

	isVblank = 1;
	vCount++; // vCount increases at each VBLANK.
	hCountBase += hCountPerVblank; // This is the "accumulated" hcount base.
	if (hCountBase > 0x7FFFFFFF) {
		hCountBase -= 0x80000000;
	}
	frameStartTicks = CoreTiming::GetTicks();

	CoreTiming::ScheduleEvent(msToCycles(vblankMs) - cyclesLate, leaveVblankEvent, vbCount + 1);

	// Trigger VBlank interrupt handlers.
	__TriggerInterrupt(PSP_INTR_IMMEDIATE | PSP_INTR_ONLY_IF_ENABLED | PSP_INTR_ALWAYS_RESCHED, PSP_VBLANK_INTR, PSP_INTR_SUB_ALL);

	// Wake up threads waiting for VBlank
	u32 error;
	bool wokeThreads = false;
	for (size_t i = 0; i < vblankWaitingThreads.size(); i++) {
		if (--vblankWaitingThreads[i].vcountUnblock == 0) {
			// Only wake it if it wasn't already released by someone else.
			SceUID waitID = __KernelGetWaitID(vblankWaitingThreads[i].threadID, WAITTYPE_VBLANK, error);
			if (waitID == 1) {
				__KernelResumeThreadFromWait(vblankWaitingThreads[i].threadID, 0);
				wokeThreads = true;
			}
			vblankWaitingThreads.erase(vblankWaitingThreads.begin() + i--);
		}
	}
	if (wokeThreads) {
		__KernelReSchedule("entered vblank");
	}

	numVBlanks++;
	numVBlanksSinceFlip++;

	// TODO: Should this be done here or in hleLeaveVblank?
	if (framebufIsLatched) {
		DEBUG_LOG(SCEDISPLAY, "Setting latched framebuffer %08x (prev: %08x)", latchedFramebuf.topaddr, framebuf.topaddr);
		framebuf = latchedFramebuf;
		framebufIsLatched = false;
		gpu->SetDisplayFramebuffer(framebuf.topaddr, framebuf.stride, framebuf.fmt);
		__DisplayFlip(cyclesLate);
	} else if (!flippedThisFrame) {
		// Gotta flip even if sceDisplaySetFramebuf was not called.
		__DisplayFlip(cyclesLate);
	}
}

void __DisplayFlip(int cyclesLate) {
	flippedThisFrame = true;
	// We flip only if the framebuffer was dirty. This eliminates flicker when using
	// non-buffered rendering. The interaction with frame skipping seems to need
	// some work.
	// But, let's flip at least once every 10 vblanks, to update fps, etc.
	const bool noRecentFlip = g_Config.iRenderingMode != FB_NON_BUFFERED_MODE && numVBlanksSinceFlip >= 10;
	// Also let's always flip for animated shaders.
	const ShaderInfo *shaderInfo = g_Config.sPostShaderName == "Off" ? nullptr : GetPostShaderInfo(g_Config.sPostShaderName);
	bool postEffectRequiresFlip = false;
	if (shaderInfo && g_Config.iRenderingMode != FB_NON_BUFFERED_MODE)
		postEffectRequiresFlip = shaderInfo->requires60fps;
	const bool fbDirty = gpu->FramebufferDirty();
	if (fbDirty || noRecentFlip || postEffectRequiresFlip) {
		CalculateFPS();

		// Let the user know if we're running slow, so they know to adjust settings.
		// Sometimes users just think the sound emulation is broken.
		static bool hasNotifiedSlow = false;
		if (!g_Config.bHideSlowWarnings && !hasNotifiedSlow && PSP_CoreParameter().fpsLimit == FPSLimit::NORMAL && IsRunningSlow()) {
#ifndef _DEBUG
			I18NCategory *err = GetI18NCategory("Error");
			if (g_Config.bSoftwareRendering) {
				host->NotifyUserMessage(err->T("Running slow: Try turning off Software Rendering"), 6.0f, 0xFF30D0D0);
			} else {
				host->NotifyUserMessage(err->T("Running slow: try frameskip, sound is choppy when slow"), 6.0f, 0xFF30D0D0);
			}
#endif
			hasNotifiedSlow = true;
		}

		// Setting CORE_NEXTFRAME causes a swap.
		const bool fbReallyDirty = gpu->FramebufferReallyDirty();
		if (fbReallyDirty || noRecentFlip || postEffectRequiresFlip) {
			// Check first though, might've just quit / been paused.
			if (coreState == CORE_RUNNING) {
				coreState = CORE_NEXTFRAME;
				gpu->CopyDisplayToOutput();
				if (fbReallyDirty) {
					actualFlips++;
				}
			}
		}

		if (fbDirty) {
			gpuStats.numFlips++;
		}

		bool throttle, skipFrame;
		DoFrameTiming(throttle, skipFrame, (float)numVBlanksSinceFlip * timePerVblank);

		int maxFrameskip = 8;
		int frameSkipNum = CalculateFrameSkip();
		if (throttle) {
			// 4 here means 1 drawn, 4 skipped - so 12 fps minimum.
			maxFrameskip = frameSkipNum;
		}
		if (numSkippedFrames >= maxFrameskip || GPURecord::IsActivePending()) {
			skipFrame = false;
		}

		if (skipFrame) {
			gstate_c.skipDrawReason |= SKIPDRAW_SKIPFRAME;
			numSkippedFrames++;
		} else {
			gstate_c.skipDrawReason &= ~SKIPDRAW_SKIPFRAME;
			numSkippedFrames = 0;
		}

		// Returning here with coreState == CORE_NEXTFRAME causes a buffer flip to happen (next frame).
		// Right after, we regain control for a little bit in hleAfterFlip. I think that's a great
		// place to do housekeeping.

		CoreTiming::ScheduleEvent(0 - cyclesLate, afterFlipEvent, 0);
		numVBlanksSinceFlip = 0;
	} else {
		// Okay, there's no new frame to draw.  But audio may be playing, so we need to time still.
		DoFrameIdleTiming();
	}
}

void hleAfterFlip(u64 userdata, int cyclesLate) {
	gpu->BeginFrame();  // doesn't really matter if begin or end of frame.

	// This seems like as good a time as any to check if the config changed.
	if (lagSyncScheduled != g_Config.bForceLagSync) {
		ScheduleLagSync();
	}
}

void hleLeaveVblank(u64 userdata, int cyclesLate) {
	isVblank = 0;
	flippedThisFrame = false;
	VERBOSE_LOG(SCEDISPLAY,"Leave VBlank %i", (int)userdata - 1);
	CoreTiming::ScheduleEvent(msToCycles(frameMs - vblankMs) - cyclesLate, enterVblankEvent, userdata);

	// Fire the vblank listeners after the vblank completes.
	__DisplayFireVblank();
}

void hleLagSync(u64 userdata, int cyclesLate) {
	// The goal here is to prevent network, audio, and input lag from the real world.
	// Our normal timing is very "stop and go".  This is efficient, but causes real world lag.
	// This event (optionally) runs every 1ms to sync with the real world.
	PROFILE_THIS_SCOPE("timing");

	if (!FrameTimingThrottled()) {
		lagSyncScheduled = false;
		return;
	}

	float scale = 1.0f;
	if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM1 && g_Config.iFpsLimit1 > 0) {
		// 0 is handled in FrameTimingThrottled().
		scale = 60.0f / g_Config.iFpsLimit1;
	} else if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM2 && g_Config.iFpsLimit2 > 0) {
		scale = 60.0f / g_Config.iFpsLimit2;
	}

	const double goal = lastLagSync + (scale / 1000.0f);
	time_update();
	// Don't lag too long ever, if they leave it paused.
	while (time_now_d() < goal && goal < time_now_d() + 0.01) {
#ifndef _WIN32
		const double left = goal - time_now_d();
		usleep((long)(left * 1000000));
#endif
		time_update();
	}

	const int emuOver = (int)cyclesToUs(cyclesLate);
	const int over = (int)((time_now_d() - goal) * 1000000);
	ScheduleLagSync(over - emuOver);
}

static u32 sceDisplayIsVblank() {
	return hleLogSuccessI(SCEDISPLAY, isVblank);
}

static int DisplayWaitForVblanks(const char *reason, int vblanks, bool callbacks = false) {
	const s64 ticksIntoFrame = CoreTiming::GetTicks() - frameStartTicks;
	const s64 cyclesToNextVblank = msToCycles(frameMs) - ticksIntoFrame;

	// These syscalls take about 115 us, so if the next vblank is before then, we're waiting extra.
	// At least, on real firmware a wait >= 16500 into the frame will wait two.
	if (cyclesToNextVblank <= usToCycles(115)) {
		++vblanks;
	}

	vblankWaitingThreads.push_back(WaitVBlankInfo(__KernelGetCurThread(), vblanks));
	__KernelWaitCurThread(WAITTYPE_VBLANK, 1, 0, 0, callbacks, reason);

	return hleLogSuccessVerboseI(SCEDISPLAY, 0, "waiting for %d vblanks", vblanks);
}

static u32 sceDisplaySetMode(int displayMode, int displayWidth, int displayHeight) {
	if (displayMode != PSP_DISPLAY_MODE_LCD || displayWidth != 480 || displayHeight != 272) {
		WARN_LOG_REPORT(SCEDISPLAY, "Video out requested, not supported: mode=%d size=%d,%d", displayMode, displayWidth, displayHeight);
	}
	if (displayMode != PSP_DISPLAY_MODE_LCD) {
		return hleLogWarning(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_MODE, "invalid mode");
	}
	if (displayWidth != 480 || displayHeight != 272) {
		return hleLogWarning(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_SIZE, "invalid size");
	}

	if (!hasSetMode) {
		gpu->InitClear();
		hasSetMode = true;
	}
	mode = displayMode;
	width = displayWidth;
	height = displayHeight;

	hleLogSuccessI(SCEDISPLAY, 0);
	// On success, this implicitly waits for a vblank start.
	return DisplayWaitForVblanks("display mode", 1);
}

void __DisplaySetFramebuf(u32 topaddr, int linesize, int pixelFormat, int sync) {
	FrameBufferState fbstate = {0};
	fbstate.topaddr = topaddr;
	fbstate.fmt = (GEBufferFormat)pixelFormat;
	fbstate.stride = linesize;

	if (sync == PSP_DISPLAY_SETBUF_IMMEDIATE) {
		// Write immediately to the current framebuffer parameters.
		framebuf = fbstate;
		gpu->SetDisplayFramebuffer(framebuf.topaddr, framebuf.stride, framebuf.fmt);
		// IMMEDIATE means that the buffer is fine. We can just flip immediately.
		// Doing it in non-buffered though creates problems (black screen) on occasion though
		// so let's not.
		if (!flippedThisFrame && g_Config.iRenderingMode != FB_NON_BUFFERED_MODE)
			__DisplayFlip(0);
	} else {
		// Delay the write until vblank
		latchedFramebuf = fbstate;
		framebufIsLatched = true;

		// If we update the format or stride, this affects the current framebuf immediately.
		framebuf.fmt = latchedFramebuf.fmt;
		framebuf.stride = latchedFramebuf.stride;
	}
}

// Some games (GTA) never call this during gameplay, so bad place to put a framerate counter.
u32 sceDisplaySetFramebuf(u32 topaddr, int linesize, int pixelformat, int sync) {
	if (sync != PSP_DISPLAY_SETBUF_IMMEDIATE && sync != PSP_DISPLAY_SETBUF_NEXTFRAME) {
		return hleLogError(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_MODE, "invalid sync mode");
	}
	if (topaddr != 0 && !Memory::IsRAMAddress(topaddr) && !Memory::IsVRAMAddress(topaddr)) {
		return hleLogError(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_POINTER, "invalid address");
	}
	if ((topaddr & 0xF) != 0) {
		return hleLogError(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_POINTER, "misaligned address");
	}
	if ((linesize & 0x3F) != 0 || (linesize == 0 && topaddr != 0)) {
		return hleLogError(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_SIZE, "invalid stride");
	}
	if (pixelformat < 0 || pixelformat > GE_FORMAT_8888) {
		return hleLogError(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_FORMAT, "invalid format");
	}

	if (sync == PSP_DISPLAY_SETBUF_IMMEDIATE) {
		if ((GEBufferFormat)pixelformat != latchedFramebuf.fmt || linesize != latchedFramebuf.stride) {
			return hleReportError(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_MODE, "must change latched framebuf first");
		}
	}

	hleEatCycles(290);

	s64 delayCycles = 0;
	// Don't count transitions between display off and display on.
	if (topaddr != 0 && topaddr != framebuf.topaddr && framebuf.topaddr != 0 && PSP_CoreParameter().compat.flags().ForceMax60FPS) {
		// sceDisplaySetFramebuf() isn't supposed to delay threads at all.  This is a hack.
		// So let's only delay when it's more than 1ms.
		const s64 FLIP_DELAY_CYCLES_MIN = usToCycles(1000);
		// Some games (like Final Fantasy 4) only call this too much in spurts.
		// The goal is to fix games where this would result in a consistent overhead.
		const int FLIP_DELAY_MIN_FLIPS = 30;
		// Since we move nextFlipCycles forward a whole frame each time, we allow it to be a little ahead.
		// Otherwise it'll always be ahead if the game messes up even once.
		const s64 LEEWAY_CYCLES_PER_FLIP = usToCycles(10);

		u64 now = CoreTiming::GetTicks();
		s64 cyclesAhead = nextFlipCycles - now;
		if (cyclesAhead > FLIP_DELAY_CYCLES_MIN) {
			if (lastFlipsTooFrequent >= FLIP_DELAY_MIN_FLIPS) {
				delayCycles = cyclesAhead;
			} else {
				++lastFlipsTooFrequent;
			}
		} else if (-lastFlipsTooFrequent < FLIP_DELAY_MIN_FLIPS) {
			--lastFlipsTooFrequent;
		}

		// 1001 to account for NTSC timing (59.94 fps.)
		u64 expected = msToCycles(1001) / 60 - LEEWAY_CYCLES_PER_FLIP;
		lastFlipCycles = now;
		nextFlipCycles = std::max(lastFlipCycles, nextFlipCycles) + expected;
	}

	__DisplaySetFramebuf(topaddr, linesize, pixelformat, sync);

	// No delaying while inside an interrupt.  It'll cause idle threads to starve.
	if (delayCycles > 0 && !__IsInInterrupt()) {
		// Okay, the game is going at too high a frame rate.  God of War and Fat Princess both do this.
		// Simply eating the cycles works and is fast, but breaks other games (like Jeanne d'Arc.)
		// So, instead, we delay this HLE thread only (a small deviation from correct behavior.)
		return hleDelayResult(hleLogSuccessI(SCEDISPLAY, 0, "delaying frame thread"), "set framebuf", cyclesToUs(delayCycles));
	} else {
		if (topaddr == 0) {
			return hleLogSuccessI(SCEDISPLAY, 0, "disabling display");
		} else {
			return hleLogSuccessI(SCEDISPLAY, 0);
		}
	}
}

bool __DisplayGetFramebuf(PSPPointer<u8> *topaddr, u32 *linesize, u32 *pixelFormat, int latchedMode) {
	const FrameBufferState &fbState = latchedMode == PSP_DISPLAY_SETBUF_NEXTFRAME ? latchedFramebuf : framebuf;
	if (topaddr != nullptr)
		(*topaddr).ptr = fbState.topaddr;
	if (linesize != nullptr)
		*linesize = fbState.stride;
	if (pixelFormat != nullptr)
		*pixelFormat = fbState.fmt;

	return true;
}

static u32 sceDisplayGetFramebuf(u32 topaddrPtr, u32 linesizePtr, u32 pixelFormatPtr, int latchedMode) {
	// NOTE: This is wrong and partially reverts #8753. Presumably there's something else involved here as well.
	// See #8816. Could also be a firmware version difference, there are a few of those...
	const FrameBufferState &fbState = (latchedMode == PSP_DISPLAY_SETBUF_NEXTFRAME && framebufIsLatched) ? latchedFramebuf : framebuf;

	if (Memory::IsValidAddress(topaddrPtr))
		Memory::Write_U32(fbState.topaddr, topaddrPtr);
	if (Memory::IsValidAddress(linesizePtr))
		Memory::Write_U32(fbState.stride, linesizePtr);
	if (Memory::IsValidAddress(pixelFormatPtr))
		Memory::Write_U32(fbState.fmt, pixelFormatPtr);

	return hleLogSuccessI(SCEDISPLAY, 0);
}

static int DisplayWaitForVblanksCB(const char *reason, int vblanks) {
	return DisplayWaitForVblanks(reason, vblanks, true);
}

static u32 sceDisplayWaitVblankStart() {
	return DisplayWaitForVblanks("vblank start waited", 1);
}

static u32 sceDisplayWaitVblank() {
	if (!isVblank) {
		return DisplayWaitForVblanks("vblank waited", 1);
	} else {
		hleEatCycles(1110);
		hleReSchedule("vblank wait skipped");
		return hleLogSuccessI(SCEDISPLAY, 1, "not waiting since in vblank");
	}
}

static u32 sceDisplayWaitVblankStartMulti(int vblanks) {
	if (vblanks <= 0) {
		return hleLogWarning(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_VALUE, "invalid number of vblanks");
	}
	if (!__KernelIsDispatchEnabled())
		return hleLogWarning(SCEDISPLAY, SCE_KERNEL_ERROR_CAN_NOT_WAIT, "dispatch disabled");
	if (__IsInInterrupt())
		return hleLogWarning(SCEDISPLAY, SCE_KERNEL_ERROR_ILLEGAL_CONTEXT, "in interrupt");

	return DisplayWaitForVblanks("vblank start multi waited", vblanks);
}

static u32 sceDisplayWaitVblankCB() {
	if (!isVblank) {
		return DisplayWaitForVblanksCB("vblank waited", 1);
	} else {
		hleEatCycles(1110);
		hleReSchedule("vblank wait skipped");
		return hleLogSuccessI(SCEDISPLAY, 1, "not waiting since in vblank");
	}
}

static u32 sceDisplayWaitVblankStartCB() {
	return DisplayWaitForVblanksCB("vblank start waited", 1);
}

static u32 sceDisplayWaitVblankStartMultiCB(int vblanks) {
	if (vblanks <= 0) {
		return hleLogWarning(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_VALUE, "invalid number of vblanks");
	}
	if (!__KernelIsDispatchEnabled())
		return hleLogWarning(SCEDISPLAY, SCE_KERNEL_ERROR_CAN_NOT_WAIT, "dispatch disabled");
	if (__IsInInterrupt())
		return hleLogWarning(SCEDISPLAY, SCE_KERNEL_ERROR_ILLEGAL_CONTEXT, "in interrupt");

	return DisplayWaitForVblanksCB("vblank start multi waited", vblanks);
}

static u32 sceDisplayGetVcount() {
	hleEatCycles(150);
	hleReSchedule("get vcount");
	return hleLogSuccessVerboseI(SCEDISPLAY, vCount);
}

static u32 __DisplayGetCurrentHcount() {
	const int ticksIntoFrame = CoreTiming::GetTicks() - frameStartTicks;
	const int ticksPerVblank = CoreTiming::GetClockFrequencyHz() / 60 / hCountPerVblank;
	// Can't seem to produce a 0 on real hardware, offsetting by 1 makes things look right.
	return 1 + (ticksIntoFrame / ticksPerVblank);
}

static u32 __DisplayGetAccumulatedHcount() {
	// The hCount is always a positive int, and wraps from 0x7FFFFFFF -> 0.
	int value = hCountBase + __DisplayGetCurrentHcount();
	return value & 0x7FFFFFFF;
}

static u32 sceDisplayGetCurrentHcount() {
	hleEatCycles(275);
	return hleLogSuccessI(SCEDISPLAY, __DisplayGetCurrentHcount());
}

static int sceDisplayAdjustAccumulatedHcount(int value) {
	if (value < 0) {
		return hleLogError(SCEDISPLAY, SCE_KERNEL_ERROR_INVALID_VALUE, "invalid value");
	}

	// Since it includes the current hCount, find the difference to apply to the base.
	u32 accumHCount = __DisplayGetAccumulatedHcount();
	int diff = value - accumHCount;
	hCountBase += diff;

	return hleLogSuccessI(SCEDISPLAY, 0);
}

static int sceDisplayGetAccumulatedHcount() {
	u32 accumHCount = __DisplayGetAccumulatedHcount();
	hleEatCycles(235);
	return hleLogSuccessI(SCEDISPLAY, accumHCount);
}

static float sceDisplayGetFramePerSec() {
	const static float framePerSec = 59.9400599f;
	VERBOSE_LOG(SCEDISPLAY,"%f=sceDisplayGetFramePerSec()", framePerSec);
	return framePerSec;	// (9MHz * 1)/(525 * 286)
}

static u32 sceDisplayIsForeground() {
	int result = hasSetMode && framebuf.topaddr != 0 ? 1 : 0;
	return hleLogSuccessI(SCEDISPLAY, result);
}

static u32 sceDisplayGetMode(u32 modeAddr, u32 widthAddr, u32 heightAddr) {
	if (Memory::IsValidAddress(modeAddr))
		Memory::Write_U32(mode, modeAddr);
	if (Memory::IsValidAddress(widthAddr))
		Memory::Write_U32(width, widthAddr);
	if (Memory::IsValidAddress(heightAddr))
		Memory::Write_U32(height, heightAddr);
	return hleLogSuccessI(SCEDISPLAY, 0);
}

static u32 sceDisplayIsVsync() {
	u64 now = CoreTiming::GetTicks();
	u64 start = frameStartTicks + msToCycles(vsyncStartMs);
	u64 end = frameStartTicks + msToCycles(vsyncEndMs);

	return hleLogSuccessI(SCEDISPLAY, now >= start && now <= end ? 1 : 0);
}

static u32 sceDisplayGetResumeMode(u32 resumeModeAddr) {
	if (Memory::IsValidAddress(resumeModeAddr))
		Memory::Write_U32(resumeMode, resumeModeAddr);
	return hleLogSuccessI(SCEDISPLAY, 0);
}

static u32 sceDisplaySetResumeMode(u32 rMode) {
	// Not sure what this does, seems to do nothing in tests and accept all values.
	resumeMode = rMode;
	return hleReportError(SCEDISPLAY, 0, "unsupported");
}

static u32 sceDisplayGetBrightness(u32 levelAddr, u32 otherAddr) {
	// Standard levels on a PSP: 44, 60, 72, 84 (AC only)

	if (Memory::IsValidAddress(levelAddr)) {
		Memory::Write_U32(brightnessLevel, levelAddr);
	}
	// Always seems to write zero?
	if (Memory::IsValidAddress(otherAddr)) {
		Memory::Write_U32(0, otherAddr);
	}
	return hleLogWarning(SCEDISPLAY, 0);
}

static u32 sceDisplaySetBrightness(int level, int other) {
	// Note: Only usable in kernel mode.
	brightnessLevel = level;
	return hleLogWarning(SCEDISPLAY, 0);
}

static u32 sceDisplaySetHoldMode(u32 hMode) {
	// Not sure what this does, seems to do nothing in tests and accept all values.
	holdMode = hMode;
	return hleReportError(SCEDISPLAY, 0, "unsupported");
}

const HLEFunction sceDisplay[] = {
	{0X0E20F177, &WrapU_III<sceDisplaySetMode>,               "sceDisplaySetMode",                 'x', "iii" },
	{0X289D82FE, &WrapU_UIII<sceDisplaySetFramebuf>,          "sceDisplaySetFrameBuf",             'x', "xiii"},
	{0XEEDA2E54, &WrapU_UUUI<sceDisplayGetFramebuf>,          "sceDisplayGetFrameBuf",             'x', "pppi"},
	{0X36CDFADE, &WrapU_V<sceDisplayWaitVblank>,              "sceDisplayWaitVblank",              'x', "",   HLE_NOT_DISPATCH_SUSPENDED },
	{0X984C27E7, &WrapU_V<sceDisplayWaitVblankStart>,         "sceDisplayWaitVblankStart",         'x', "",   HLE_NOT_IN_INTERRUPT | HLE_NOT_DISPATCH_SUSPENDED },
	{0X40F1469C, &WrapU_I<sceDisplayWaitVblankStartMulti>,    "sceDisplayWaitVblankStartMulti",    'x', "i"   },
	{0X8EB9EC49, &WrapU_V<sceDisplayWaitVblankCB>,            "sceDisplayWaitVblankCB",            'x', "",   HLE_NOT_DISPATCH_SUSPENDED },
	{0X46F186C3, &WrapU_V<sceDisplayWaitVblankStartCB>,       "sceDisplayWaitVblankStartCB",       'x', "",   HLE_NOT_IN_INTERRUPT | HLE_NOT_DISPATCH_SUSPENDED },
	{0X77ED8B3A, &WrapU_I<sceDisplayWaitVblankStartMultiCB>,  "sceDisplayWaitVblankStartMultiCB",  'x', "i"   },
	{0XDBA6C4C4, &WrapF_V<sceDisplayGetFramePerSec>,          "sceDisplayGetFramePerSec",          'f', ""    },
	{0X773DD3A3, &WrapU_V<sceDisplayGetCurrentHcount>,        "sceDisplayGetCurrentHcount",        'x', ""    },
	{0X210EAB3A, &WrapI_V<sceDisplayGetAccumulatedHcount>,    "sceDisplayGetAccumulatedHcount",    'i', ""    },
	{0XA83EF139, &WrapI_I<sceDisplayAdjustAccumulatedHcount>, "sceDisplayAdjustAccumulatedHcount", 'i', "i"   },
	{0X9C6EAAD7, &WrapU_V<sceDisplayGetVcount>,               "sceDisplayGetVcount",               'x', ""    },
	{0XDEA197D4, &WrapU_UUU<sceDisplayGetMode>,               "sceDisplayGetMode",                 'x', "ppp" },
	{0X7ED59BC4, &WrapU_U<sceDisplaySetHoldMode>,             "sceDisplaySetHoldMode",             'x', "x"   },
	{0XA544C486, &WrapU_U<sceDisplaySetResumeMode>,           "sceDisplaySetResumeMode",           'x', "x"   },
	{0XBF79F646, &WrapU_U<sceDisplayGetResumeMode>,           "sceDisplayGetResumeMode",           'x', "p"   },
	{0XB4F378FA, &WrapU_V<sceDisplayIsForeground>,            "sceDisplayIsForeground",            'x', ""    },
	{0X31C4BAA8, &WrapU_UU<sceDisplayGetBrightness>,          "sceDisplayGetBrightness",           'x', "pp"  },
	{0X9E3C6DC6, &WrapU_II<sceDisplaySetBrightness>,          "sceDisplaySetBrightness",           'x', "ii"  },
	{0X4D4E10EC, &WrapU_V<sceDisplayIsVblank>,                "sceDisplayIsVblank",                'x', ""    },
	{0X21038913, &WrapU_V<sceDisplayIsVsync>,                 "sceDisplayIsVsync",                 'x', ""    },
};

void Register_sceDisplay() {
	RegisterModule("sceDisplay", ARRAY_SIZE(sceDisplay), sceDisplay);
}

void Register_sceDisplay_driver() {
	RegisterModule("sceDisplay_driver", ARRAY_SIZE(sceDisplay), sceDisplay);
}
