1// ð Loop 2 3// These numbers define the budgeted frame-time (max) for CPU and Rendering. 4// The updates will repeat multiple times per frame, but rendering will only 5// ever happen once per display refresh. 6 7const updateFps = /*120*/120; // This is constant and should be used for interpolation. 8let renderFps = 165; // This is a maximum and will vary across environments. 9const updateRate = 1000 / updateFps; 10let renderRate = 1000 / renderFps; 11let updateTime = 0; 12let renderTime = 0; 13let lastNow; 14let input; 15let updateAndRender; 16let paused = false; 17 18// â±ï¸ Background Timer Worker 19// When the page is hidden and a piece opts into `background: true`, we use a 20// Web Worker setInterval to keep pumping sim-only frames. Worker timers are 21// NOT throttled by the browser, unlike rAF (paused) or main-thread timers (1/sec). 22let timerWorker = null; 23let backgroundMode = false; // true when hidden AND piece wants background ticks 24let backgroundEnabled = false; // set by bios when piece exports `background: true` 25 26function ensureTimerWorker() { 27 if (timerWorker) return timerWorker; 28 try { 29 let workerUrl; 30 try { 31 workerUrl = new URL("./timer-worker.mjs", import.meta.url); 32 } catch { 33 // In pack mode (blob: URLs), import.meta.url can't resolve relative paths. 34 return null; 35 } 36 timerWorker = new Worker(workerUrl, { type: "module" }); 37 timerWorker.onmessage = onTimerWorkerTick; 38 } catch (e) { 39 console.warn("â±ï¸ Timer worker failed to start:", e); 40 } 41 return timerWorker; 42} 43 44function onTimerWorkerTick(e) { 45 if (e.data.type !== "tick" || paused || !backgroundMode) return; 46 // Drive a sim-only frame (no render). 47 const now = performance.now(); 48 const rawDelta = now - lastNow; 49 // Clamp delta: the main thread only wakes ~1/sec when hidden. 50 // Cap to 50ms so we produce at most ~6 sim ticks per wake-up, 51 // avoiding massive bursts that confuse piece timing heuristics. 52 const delta = Math.min(rawDelta, 50); 53 updateTime += delta; 54 lastNow = now; 55 56 let updateTimes = 0; 57 while (updateTime >= updateRate) { 58 updateTimes += 1; 59 updateTime -= updateRate; 60 } 61 62 if (updateTimes > 0) { 63 // needsRender = false â sim only, no paint 64 updateAndRender(false, updateTimes, now); 65 } 66} 67 68function startBackgroundTicks() { 69 if (backgroundMode) return; 70 backgroundMode = true; 71 // Reset accumulators so the first background tick doesn't catch up 72 updateTime = 0; 73 lastNow = performance.now(); 74 const w = ensureTimerWorker(); 75 if (w) w.postMessage({ type: "start", rate: 8 }); // ~120fps 76} 77 78function stopBackgroundTicks() { 79 if (!backgroundMode) return; 80 backgroundMode = false; 81 if (timerWorker) timerWorker.postMessage({ type: "stop" }); 82} 83 84// Input runs once per loop. 85// Update runs multiple times. 86// Render runs once if enough time has passed. 87function loop(now, XR = false) { 88 if (paused) { 89 lastNow = now; 90 if (!XR) window.requestAnimationFrame(loop); 91 return; 92 } 93 94 input(now); 95 96 const delta = now - lastNow; 97 98 updateTime += delta; 99 renderTime += delta; 100 lastNow = now; 101 102 let updateTimes = 0; 103 104 while (updateTime >= updateRate) { 105 updateTimes += 1; 106 updateTime -= updateRate; 107 } 108 109 let needsRender = false; 110 111 if (renderTime >= renderRate) { 112 needsRender = true; 113 renderTime -= renderRate; 114 } 115 116 updateAndRender(needsRender, updateTimes, now); 117 118 // We don't use requestAnimationframe when 119 // running the main loop in an VR/AR/XR environment. 120 if (!XR) window.requestAnimationFrame(loop); 121} 122 123// Handle visibility changes: 124// - When hidden + backgroundEnabled â start timer worker for sim-only frames 125// - When visible â stop timer worker, reset accumulators to prevent catch-up 126document.addEventListener("visibilitychange", function () { 127 if (document.hidden) { 128 if (backgroundEnabled) { 129 startBackgroundTicks(); 130 } 131 } else { 132 stopBackgroundTicks(); 133 updateTime = 0; 134 renderTime = 0; // Also reset render time to prevent catch-up rendering 135 lastNow = performance.now(); 136 } 137}); 138 139// Allow bios to enable/disable background mode for the current piece.
140function setBackgroundEnabled(enabled) { 141 backgroundEnabled = !!enabled; 142 // If we're already hidden and background just got enabled, start ticking. 143 if (backgroundEnabled && document.hidden) { 144 startBackgroundTicks(); 145 } else if (!backgroundEnabled) { 146 stopBackgroundTicks(); 147 } 148} 149 150// Start the loop. 151function start(inputFun, updateAndRenderFun) { 152 input = inputFun; 153 updateAndRender = updateAndRenderFun; 154 lastNow = performance.now(); 155 paused = false; 156 window.requestAnimationFrame(loop); 157} 158 159function pause() { 160 paused = true; 161} 162 163function resume() { 164 paused = false; 165 lastNow = performance.now(); 166 updateTime = 0; 167} 168 169// Update the frame rate. 170function frameRate(n = 165) { 171 if (renderFps === n) return; // Skip if framerate hasn't changed 172 renderFps = n; 173 if (renderFps !== 165) console.log("ðï¸ FPS:", renderFps); 174 renderRate = 1000 / renderFps; 175 renderTime = 0; 176} 177 178export const mainLoop = loop; 179export { start, frameRate, pause, resume, setBackgroundEnabled };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.