mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
Manually handle frameskipping for better audio on non multiple of 60hz screens (#613)
* We want to always use animation frames as our time to call the main
loop because otherwise the timing is inconsistent leading to very bad
audio.
* We don't want to call the main loop every frame otherwise > 60hz
screens will play too fast.
* We can't emscripten's frameskipping becuase it only allows for
integers whereas screeens come in many other framerates such as 75hz,
90hz, 144hz, etc.
* Therefore we manually manage frameskipping by ~~ab~~using pause and
resume game which internally just enable and disable the emscripten's
calling of the main loop. (see
27dede256b/src/platform/wasm/main.c (L183)
).
* We could modify the code to let us call the main loop ourselves and
take full control over this, but for now this is okay.
This commit is contained in:
commit
9b36ae62e0
|
@ -5,10 +5,10 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import mGBA from "./vendor/mgba";
|
import mGBA, { mGBAEmulator } from "./vendor/mgba";
|
||||||
import { GbaKey, KeyBindings } from "./bindings";
|
import { GbaKey, KeyBindings } from "./bindings";
|
||||||
import { styled } from "styled-components";
|
import { styled } from "styled-components";
|
||||||
import { useSmoothedFramerate } from "./useSmoothedFramerate.hook";
|
import { useFrameSkip } from "./useFrameSkip.hook";
|
||||||
|
|
||||||
type Module = any;
|
type Module = any;
|
||||||
|
|
||||||
|
@ -41,18 +41,10 @@ export interface MgbaHandle {
|
||||||
buttonRelease: (key: GbaKey) => void;
|
buttonRelease: (key: GbaKey) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const whichFrameSkip = (frameRate: number): number | undefined => {
|
|
||||||
if ((frameRate + 5) % 60 <= 10) {
|
|
||||||
// framerate close to multiple of 60
|
|
||||||
// use frameskip
|
|
||||||
return Math.round(frameRate / 60);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
|
export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
|
||||||
({ gameUrl, volume, controls, paused }, ref) => {
|
({ gameUrl, volume, controls, paused }, ref) => {
|
||||||
const canvas = useRef(null);
|
const canvas = useRef(null);
|
||||||
const mgbaModule = useRef<Module>({});
|
const mgbaModule = useRef<Module>({} as mGBAEmulator);
|
||||||
|
|
||||||
const [state, setState] = useState(MgbaState.Uninitialised);
|
const [state, setState] = useState(MgbaState.Uninitialised);
|
||||||
const [gameLoaded, setGameLoaded] = useState(false);
|
const [gameLoaded, setGameLoaded] = useState(false);
|
||||||
|
@ -101,23 +93,7 @@ export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
|
||||||
};
|
};
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
const frameRate = useSmoothedFramerate();
|
useFrameSkip(mgbaModule);
|
||||||
const frameSkipToUse = whichFrameSkip(frameRate);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!gameLoaded) return;
|
|
||||||
|
|
||||||
if (frameSkipToUse) {
|
|
||||||
// framerate close to multiple of 60
|
|
||||||
// use frameskip
|
|
||||||
console.log("Using frameskip");
|
|
||||||
mgbaModule.current.setMainLoopTiming(1, frameSkipToUse);
|
|
||||||
} else {
|
|
||||||
// frame rate not close to multiple of 60, use timeout
|
|
||||||
console.log("Using timeout");
|
|
||||||
mgbaModule.current.setMainLoopTiming(0, 1000 / 59.727500569606);
|
|
||||||
}
|
|
||||||
}, [frameSkipToUse, gameLoaded]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gameLoaded) return;
|
if (!gameLoaded) return;
|
||||||
|
|
54
website/app/src/useFrameSkip.hook.ts
Normal file
54
website/app/src/useFrameSkip.hook.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { MutableRefObject, useEffect } from "react";
|
||||||
|
import { mGBAEmulator } from "./vendor/mgba";
|
||||||
|
|
||||||
|
|
||||||
|
export const useFrameSkip = (mgbaModule: MutableRefObject<mGBAEmulator>) => {
|
||||||
|
useEffect(() => {
|
||||||
|
let previous: number | undefined = undefined;
|
||||||
|
let stopped = false;
|
||||||
|
let smoothedFrameTime = 60;
|
||||||
|
|
||||||
|
let totalTime = 0;
|
||||||
|
let paused = false;
|
||||||
|
|
||||||
|
const raf = (time: DOMHighResTimeStamp) => {
|
||||||
|
if (previous) {
|
||||||
|
const delta = time - previous;
|
||||||
|
console.log(delta);
|
||||||
|
|
||||||
|
smoothedFrameTime = (smoothedFrameTime * 3 + delta) / 4;
|
||||||
|
|
||||||
|
const smoothedFrameRate = Math.round(1 / (smoothedFrameTime / 1000));
|
||||||
|
|
||||||
|
|
||||||
|
totalTime += 1 / smoothedFrameRate;
|
||||||
|
|
||||||
|
if (totalTime >= 1 / 60) {
|
||||||
|
totalTime -= 1 / 60;
|
||||||
|
if (paused) {
|
||||||
|
mgbaModule.current.resumeGame();
|
||||||
|
paused = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!paused) {
|
||||||
|
mgbaModule.current.pauseGame();
|
||||||
|
paused = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
previous = time;
|
||||||
|
|
||||||
|
if (!stopped) {
|
||||||
|
window.requestAnimationFrame(raf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(raf);
|
||||||
|
return () => { stopped = true; }
|
||||||
|
}, [mgbaModule]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,35 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
|
|
||||||
export const useSmoothedFramerate = (): number => {
|
|
||||||
|
|
||||||
const [smoothedFrameTime, setSmoothedFrameTime] = useState(60);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
let previous: number | undefined = undefined;
|
|
||||||
let stopped = false;
|
|
||||||
|
|
||||||
const raf = (time: DOMHighResTimeStamp) => {
|
|
||||||
if (previous) {
|
|
||||||
let delta = time - previous;
|
|
||||||
|
|
||||||
setSmoothedFrameTime((time) => (time * 3 + delta) / 4);
|
|
||||||
}
|
|
||||||
previous = time;
|
|
||||||
|
|
||||||
|
|
||||||
if (!stopped) {
|
|
||||||
window.requestAnimationFrame(raf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.requestAnimationFrame(raf);
|
|
||||||
|
|
||||||
return () => { stopped = true; }
|
|
||||||
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
return Math.round(1 / (smoothedFrameTime / 1000));
|
|
||||||
}
|
|
Loading…
Reference in a new issue