60 FPS にする時に各フレームは何秒にするのが理想的かを計算する

その昔、Win32 でゲームを作ってた頃に timeGetTime() と sleep() で正確に 60 FPS にするには各フレームでどれだけ時間をとればいいのか考えた事があった。
(ちなみに、確か Direct2D に画面フリップまでブロックする処理があって、そっちを使った方が画面は崩れなかった気がする)

今は可変 FPS の時代だし、あんまり意味はなさそうだけど、なんかの参考になるかもしれないのでメモっておく。

大枠の考え方としては「きれいにバラけるように事前に表を作っておいて、各フレームではその表の時間だけ使っていいとする」みたいな感じ。

その表を作る部分は当時「こういう表が欲しいわけだから……」というところから逆算して、これでいけるな！と思った方法という、説明しがたいやつだったが、今回 HTML にべた書きした JavaScript でその表を計算して表示するので、実行して存分に堪能して欲しい。

```ideal-frame-duration-calculation.html
<html>
    <head>
        <title>ideal frame duration calculation</title>
        <script>
(function(global){
    // 二つの数の最大公約数 (greatest common divisor) を求めます.
    const gcd2 = function(value1, value2) {
        if ((value1 == 0) || (value2 == 0)) {
            // 想定外の引数
            throw "illegal argument";
        }
        
        // ユーグリッドの互除法 (Euclidean Algorithm)
        while (value2 != 0) {
            const tmpValue = value2;
            value2 = value1 % value2;
            value1 = tmpValue;
        };
        
        return value1;
    };
    
    // // 二つの数の最小公倍数 (least common multiple) を求めます.
    // const lcm2 = function(value1, value2) {
    //     
    //     if ((value1 == 0) || (value2 == 0)) {
    //         // 想定外の引数
    //         throw "illegal argument";
    //     }
    //     
    //     const v1 = value1 * value2;
    //     const v2 = gcd2(value1, value2);
    //     const result = parseInt(v1 / v2);
    //     return result;
    // };
    
    // `createCycleTable` 理想 FPS の為に各フレームで待つべき時間の表を作成します.
    // `waitsPerSecond` 一秒辺りの待ち時間 (ミリ秒単位なら 1000, マイクロ秒単位なら 1000000)
    // `fpsToDesire` 理想秒間フレーム数
    const createCycleTable = function(waitsPerSecond, fpsToDesire) {
        const baseTime = parseInt(waitsPerSecond / fpsToDesire);
        const remaindered = waitsPerSecond % fpsToDesire;
        if (remaindered == 0) {
            return [baseTime];
        }
        
        const gcdValue = gcd2(fpsToDesire, remaindered);
        const cycleItemCount = parseInt(fpsToDesire / gcdValue);
        const longItemCount = parseInt(remaindered / gcdValue);
        
        var cycleTable = [];
        const mag = 65536;
        const toDecrease = parseInt(mag * longItemCount / cycleItemCount);
        var counter = mag * longItemCount;
        for (var i = 0; i < cycleItemCount; i += 1)
        {
            const prev = parseInt(counter / mag);
            counter -= toDecrease;
            const curr = parseInt(counter / mag);
            if (prev != curr) {
                cycleTable[i] = baseTime + 1;
            } else {
                cycleTable[i] = baseTime;
            }
        }
        
        // 確かめ算 verification of figures
        var cntForVerification = 0;
        var pos = 0;
        for (var i = 0; i < fpsToDesire; i += 1) {
            cntForVerification += cycleTable[pos];
            pos += 1;
            pos %= cycleItemCount;
        }
        
        if (cntForVerification != waitsPerSecond) {
            throw `verification failed. expects: ${waitsPerSecond}, but actual: ${cntForVerification}`;
        }
        
        return cycleTable;
    };
    
    document.addEventListener('DOMContentLoaded', function() {
        const waitsPerSecondBox = document.querySelector('#waits-per-second-box');
        const fpsToDesireBox = document.querySelector('#fps-to-desire-box');
        const calcTableButton = document.querySelector('#calc-table-button');
        const resultPane = document.querySelector('#result-pane');
        
        
        calcTableButton.addEventListener('click', function(event){
            const waitsPerSecond = parseInt(waitsPerSecondBox.value, 10);
            const fpsToDesire = parseInt(fpsToDesireBox.value, 10);
            const cycleTable = createCycleTable(waitsPerSecond, fpsToDesire);
            const cycleItemCount = cycleTable.length;
            
            // 結果の表を作る (ここは出りゃいいでしょという適当なやり方)
            const tableElm = document.createElement('table');
            for (var i = 0; i < cycleItemCount; i = i + 1) {
                const cycleItem = cycleTable[i];
                
                const td1Elm = document.createElement('td');
                td1Elm.innerText = `${i + 1}`;
                const td2Elm = document.createElement('td');
                td2Elm.innerText = `${cycleItem}`;
                const trElm = document.createElement('tr');
                trElm.appendChild(td1Elm);
                trElm.appendChild(td2Elm);
                tableElm.appendChild(trElm);
            }
            resultPane.innerHTML = '';
            resultPane.appendChild(tableElm);
            
            tickCycleTable = cycleTable;
            tickCycleTableIndex = 0;
        });
        
        onTick();
    });
    
    const getTickCount = function() {
        return (new Date()).getTime();
    };
    
    var progress = 0;
    var increment = 15;
    var max = 360;
    
    var tickCycleTable = [1000];
    var tickCycleTableIndex = 0;
    var tickCount = getTickCount();
    const onTick = function() {
        const thisTickStartedAt = getTickCount();
        const interval = tickCycleTable[tickCycleTableIndex];
        tickCycleTableIndex += 1;
        tickCycleTableIndex %= tickCycleTable.length;
        // console.log(tickCycleTableIndex);
        const nextTickShouldStartAt = tickCount + interval;
        tickCount += interval;
        // ----
        
        const hoge = Math.sin(progress * (Math.PI / 180));
        
        const demoCanvas = document.querySelector('#demo-canvas');
        const ctx2d = demoCanvas.getContext('2d');
        ctx2d.fillStyle = 'white';
        ctx2d.fillRect(0, 0, 100, 100);
        
        ctx2d.fillStyle = 'blue';
        ctx2d.beginPath();
        ctx2d.arc(50, 50 + (10 * hoge), 20, 0, Math.PI * 2, true);
        ctx2d.closePath();
        ctx2d.fill();
        
        // console.log(`${progress} ${mage}`);
        
        progress += increment;
        progress %= max;
        
        // ----
        const tickProcessEndedAt = getTickCount();
        var delay = nextTickShouldStartAt - tickProcessEndedAt;
        if (delay < 0) {
            console.log('間に合わない');
            tickCount = tickProcessEndedAt;
            delay = interval;
        }
        
        setTimeout(onTick, delay);
    };
})(this);
        </script>
        <style>
            table { border: 1px solid; }
            td { border: 1px solid; }
        </style>
    </head>
    <body>
        <div>
            <label for="waits-per-second-box">Waits per second</label>
            <input id="waits-per-second-box" type="text" id="" value="1000" />
            <span>一秒辺りの待ち時間 (ミリ秒単位なら 1000, マイクロ秒単位なら 1000000)</span>
        </div>
        <div>
            <label for="fps-to-desire-box">FPS to desire</label>
            <input id="fps-to-desire-box" type="text" id="" value="60" />
            <span>理想秒間フレーム数</span>
        </div>
        <div>
            <button id="calc-table-button">Calculate</button>
        </div>
        <div>
            <label for="demo-canvas">demo</label>
            <canvas id="demo-canvas" width="100" height="100"></canvas>
        </div>
        <div id="result-pane">
        </div>
    </body>
</html>
```

「数学は無理、算数の時点で超苦手」という自分が何故最大公約数にたどり着いたのか、本当に覚えてない。
(でも一人でゼロから売り物の 3D のアクション RPG (BREW by KDDI な奴のアプリ) を書いた事あるんですよ！描画はライブラリがやってくれたけど、内部計算はガチで自力で全部やったの！ってか malloc/free 相当のやつも自作した。自分でも信じられない！)