Split The code is public; the app answers only to the crew.

Hold 60 fps on a laptop and a phone: adapt the render scale to the frame time #12

shipped merged opened this has it
opened this

Trade pixels for frames automatically, so the game holds 60 fps on a laptop's integrated GPU and on a phone instead of rendering at full resolution and dropping to 30.

resize() in src/client/renderer.ts sizes the drawing buffer at min(devicePixelRatio, 2) and never reconsiders. On a 3x phone screen that is a 4x pixel budget for a fragment shader that does a texture-array fetch and a fog term per pixel, and there is nothing in the loop that reacts when a frame takes 30 ms. main.ts already smooths the frame time into frameAverage and prints it into #fps, so the measurement exists and nothing uses it.

Make the scale a decision, not a constant: a pure function that takes the current scale and a smoothed frame time and returns the next scale, stepping down when frames run long and back up when they are comfortably short, clamped so it can never collapse or oscillate. Put it in its own tiny module so it is testable — renderer.ts needs a GL context and is one of the untested modules.

Done looks like

  • [ ] A pure nextRenderScale(scale, frameMs) in its own module, clamped to a sane range (0.5 to 1 of the capped device ratio), with a dead band so a frame time near the target does not make it hunt.
  • [ ] renderer.resize() multiplies the device ratio by the current scale; the HUD canvas keeps its own crisp resolution so text does not go soft.
  • [ ] The loop updates the scale at most a few times a second, from the smoothed average, not from one frame.
  • [ ] The #fps readout says the scale when it is below 1, so a slow machine is legible rather than mysterious.
  • [ ] No layout shift and no change to the CSS size of #stage: only the drawing buffer changes.
  • [ ] A measurement in the pull: frame time at 1280x720 and at 360px wide, before and after, on whatever machine the builder has — the point of the change is a number.
  • [ ] bun test covers the function: long frames lower the scale, short frames raise it, it never leaves the clamp, and a steady frame time at the target leaves it alone.
  • [ ] bun run scripts/build.ts run and the rebuilt bundle committed.

Files I expect to change

src/client/quality.ts (new), src/client/quality.test.ts (new), src/client/renderer.ts, src/client/main.ts, static/app.<hash>.js, and a line in the README's shape list.

Risks

Scaling on the same frame the browser resizes the canvas causes a visible pop; change it on a timer and let the clamp keep the steps coarse. Raising the scale as soon as frames are short while the load is about to spike is the classic oscillation — the dead band and the rate limit are what stop it. The reported fps must stay honest: it is the frame time, not the scale, that says whether the game is smooth.

Who

A builder who will actually profile rather than guess. It touches the same frame loop as the input-latch issue, so it goes after it.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs