using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using BizHawk.Client.Common; //this throttle is nitsuja's fine-tuned techniques from desmume namespace BizHawk.Client.EmuHawk { public class Throttle { int lastskiprate; int framestoskip; int framesskipped; public bool skipnextframe; //if the emulator is paused then we dont need to behave as if unthrottled public bool signal_paused; public bool signal_frameAdvance; public bool signal_unthrottle; public bool signal_continuousframeAdvancing; //continuousframeAdvancing public bool signal_overrideSecondaryThrottle; public int cfg_frameskiprate { get { return Global.Config.FrameSkip; } } public bool cfg_frameLimit { get { return Global.Config.ClockThrottle; } } public bool cfg_autoframeskipenab { get { return Global.Config.AutoMinimizeSkipping; } } public void Step(bool allowSleep, int forceFrameSkip) { //TODO - figure out what allowSleep is supposed to be used for //TODO - figure out what forceFrameSkip is supposed to be used for bool extraThrottle = false; //if we're paused, none of this should happen. just clean out our state and dont skip //notably, if we're frame-advancing, we should be paused. if (signal_paused && !signal_continuousframeAdvancing) { //Console.WriteLine("THE THING: {0} {1}", signal_paused ,signal_continuousframeAdvancing); skipnextframe = false; framesskipped = 0; framestoskip = 0; return; } //heres some ideas for how to begin cleaning this up ////at this point, its assumed that we're running. ////this could be a free run, an unthrottled run, or a 'continuous frame advance' (aka continuous) run ////free run: affected by frameskips and throttles ////unthrottled run: affected by frameskips only ////continuous run: affected by frameskips and throttles ////so continuous and free are the same? //bool continuous_run = signal_continuousframeAdvancing; //bool unthrottled_run = signal_unthrottle; //bool free_run = !continuous_run && !unthrottled_run; //bool do_throttle, do_skip; //if (continuous_run || free_run) // do_throttle = do_skip = true; //else if (unthrottled_run) // do_skip = true; //else throw new InvalidOperationException(); int skipRate = (forceFrameSkip < 0) ? cfg_frameskiprate : forceFrameSkip; int ffSkipRate = (forceFrameSkip < 0) ? 3 : forceFrameSkip; if (lastskiprate != skipRate) { lastskiprate = skipRate; framestoskip = 0; // otherwise switches to lower frameskip rates will lag behind } if (!skipnextframe || forceFrameSkip == 0 || (signal_continuousframeAdvancing && !signal_unthrottle)) { framesskipped = 0; if (signal_continuousframeAdvancing) { //dont ever skip frames when continuous frame advancing. it's meant for precision work. //but we DO need to throttle if(Global.Config.ClockThrottle) extraThrottle = true; } else { if (framestoskip > 0) skipnextframe = true; } } else { framestoskip--; if (framestoskip < 1) skipnextframe = false; else skipnextframe = true; framesskipped++; } if (signal_unthrottle) { if (framesskipped < ffSkipRate) { skipnextframe = true; framestoskip = 1; } if (framestoskip < 1) framestoskip += ffSkipRate; } else if ((extraThrottle || signal_paused || /*autoframeskipenab && frameskiprate ||*/ cfg_frameLimit || signal_overrideSecondaryThrottle) && allowSleep) { SpeedThrottle(signal_paused); } if (cfg_autoframeskipenab && cfg_frameskiprate != 0) { if (!signal_continuousframeAdvancing) { AutoFrameSkip_NextFrame(); if (framestoskip < 1) framestoskip += AutoFrameSkip_GetSkipAmount(0, skipRate); } } else { if (framestoskip < 1) framestoskip += skipRate; } } static ulong GetCurTime() { if (tmethod == 1) return (ulong)Stopwatch.GetTimestamp(); else return (ulong)Environment.TickCount; } #if WINDOWS [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")] static extern uint timeBeginPeriod(uint uMilliseconds); #endif static readonly int tmethod; static readonly ulong afsfreq; static readonly ulong tfreq; static Throttle() { #if WINDOWS timeBeginPeriod(1); #endif if (Stopwatch.IsHighResolution) { afsfreq = (ulong)Stopwatch.Frequency; tmethod = 1; } else { afsfreq = 1000; tmethod = 0; } Console.WriteLine("throttle method: {0}; resolution: {1}", tmethod, afsfreq); tfreq = afsfreq * 65536; } public void SetCoreFps(double desired_fps) { core_desiredfps = (ulong)(65536 * desired_fps); int target_pct = pct; pct = -1; SetSpeedPercent(target_pct); } int pct = -1; public void SetSpeedPercent(int percent) { //Console.WriteLine("throttle set percent " + percent); if (pct == percent) return; pct = percent; float fraction = percent / 100.0f; desiredfps = (ulong)(core_desiredfps * fraction); //Console.WriteLine("throttle set desiredfps " + desiredfps); desiredspf = 65536.0f / desiredfps; AutoFrameSkip_IgnorePreviousDelay(); } ulong core_desiredfps; ulong desiredfps; float desiredspf; ulong ltime; ulong beginticks, endticks, preThrottleEndticks; float fSkipFrames; float fSkipFramesError; int lastSkip; float lastError; float integral; public void AutoFrameSkip_IgnorePreviousDelay() { beginticks = GetCurTime(); // this seems to be a stable way of allowing the skip frames to // quickly adjust to a faster environment (e.g. after a loadstate) // without causing oscillation or a sudden change in skip rate fSkipFrames *= 0.5f; } void AutoFrameSkip_BeforeThrottle() { preThrottleEndticks = GetCurTime(); } void AutoFrameSkip_NextFrame() { endticks = GetCurTime(); // calculate time since last frame ulong diffticks = endticks - beginticks; float diff = (float)diffticks / afsfreq; // calculate time since last frame not including throttle sleep time if (preThrottleEndticks == 0) // if we didn't throttle, use the non-throttle time preThrottleEndticks = endticks; ulong diffticksUnthrottled = preThrottleEndticks - beginticks; float diffUnthrottled = (float)diffticksUnthrottled / afsfreq; float error = diffUnthrottled - desiredspf; // reset way-out-of-range values if (diff > 1) diff = 1; if (error > 1 || error < -1) error = 0; if (diffUnthrottled > 1) diffUnthrottled = desiredspf; float derivative = (error - lastError) / diff; lastError = error; integral = integral + (error * diff); integral *= 0.99f; // since our integral isn't reliable, reduce it to 0 over time. // "PID controller" constants // this stuff is probably being done all wrong, but these seem to work ok const float Kp = 40.0f; const float Ki = 0.55f; const float Kd = 0.04f; float errorTerm = error * Kp; float derivativeTerm = derivative * Kd; float integralTerm = integral * Ki; float adjustment = errorTerm + derivativeTerm + integralTerm; // apply the output adjustment fSkipFrames += adjustment; // if we're running too slowly, prevent the throttle from kicking in if (adjustment > 0 && fSkipFrames > 0) ltime -= tfreq / desiredfps; preThrottleEndticks = 0; beginticks = GetCurTime(); } int AutoFrameSkip_GetSkipAmount(int min, int max) { int rv = (int)fSkipFrames; fSkipFramesError += fSkipFrames - rv; // resolve accumulated fractional error // where doing so doesn't push us out of range while (fSkipFramesError >= 1.0f && rv <= lastSkip && rv < max) { fSkipFramesError -= 1.0f; rv++; } while (fSkipFramesError <= -1.0f && rv >= lastSkip && rv > min) { fSkipFramesError += 1.0f; rv--; } // restrict skip amount to requested range if (rv < min) rv = min; if (rv > max) rv = max; // limit maximum error accumulation (it's mainly only for fractional components) if (fSkipFramesError >= 4.0f) fSkipFramesError = 4.0f; if (fSkipFramesError <= -4.0f) fSkipFramesError = -4.0f; // limit ongoing skipframes to requested range + 1 on each side if (fSkipFrames < min - 1) fSkipFrames = (float)min - 1; if (fSkipFrames > max + 1) fSkipFrames = (float)max + 1; // printf("%d", rv); lastSkip = rv; return rv; } void SpeedThrottle(bool paused) { AutoFrameSkip_BeforeThrottle(); ulong timePerFrame = tfreq / desiredfps; while (true) { if (signal_unthrottle) return; ulong ttime = GetCurTime(); ulong elapsedTime = ttime - ltime; if (elapsedTime >= timePerFrame) { int maxMissedFrames = (int)Math.Ceiling((Global.SoundMaxBufferDeficitMs / 1000.0) / ((double)timePerFrame / afsfreq)); if (maxMissedFrames < 3) maxMissedFrames = 3; if (elapsedTime > timePerFrame * (ulong)(1 + maxMissedFrames)) ltime = ttime; else ltime += timePerFrame; return; } int sleepTime = (int)((timePerFrame - elapsedTime) * 1000 / afsfreq); if (sleepTime >= 2 || paused) { #if WINDOWS // Assuming a timer period of 1 ms (i.e. timeBeginPeriod(1)): The actual sleep time // on Windows XP is generally within a half millisecond either way of the requested // time. The actual sleep time on Windows 8 is generally between the requested time // and up to a millisecond over. So we'll subtract 1 ms from the time to avoid // sleeping longer than desired. sleepTime -= 1; #else // The actual sleep time on OS X with Mono is generally between the request time // and up to 25% over. So we'll scale the sleep time back to account for that. sleepTime = sleepTime * 4 / 5; #endif Thread.Sleep(Math.Max(sleepTime, 1)); } else if (sleepTime > 0) // spin for <1 millisecond waits { Thread.Yield(); // limit to other threads on the same CPU core for other short waits } } } } }