benjivers/val Public Public: anyone can read this; the crew can change it.

The renderer is pixel-bound on a retina screen: cap the internal resolution and batch the wall slices #14

Closed opened this has it
opened this

It feels worse since the smoothing work, and the network is not the reason: production and the live preview both deliver a steady 30.2 Hz, p99 40.8ms, zero gaps over 100ms. The cost is in the frame, and the numbers are unkind on a retina Mac.

resize() sizes the canvas at devicePixelRatio capped to 2, so a 1512pt window is a 3024px canvas. scene.js:81 then picks step = W > 1100 ? 2 : 1, which is 1512 rays per frame — and one fillRect per column for the wall slice, plus the sprite columns on top. At 60fps that is ~91,000 ray marches and ~91,000 canvas fills a second. Fullscreen on a large display it is nearer 150,000. That is the choppiness, and doubling the snapshot rate added parse work on top of an already-full frame.

Three things, in the order I would do them:

1. Render the 3D view at a fixed internal width, then blit it up. A raycaster does not need device pixels: draw the world into an offscreen canvas of, say, 960px (or 1280 on a wide window), then drawImage it to the visible canvas scaled. Walls are flat-shaded columns, so upscaling costs almost nothing visually and cuts the ray count two to three times immediately. Keep the HUD, the crosshair and the minimap drawn at full resolution on the visible canvas, where sharpness actually shows.

2. Batch the wall slices by run. The colours are already quantised into levels (scene.js says so). Walk the columns and emit one fillRect per run of columns sharing a level and a wall face, instead of one per column. In a corridor that collapses hundreds of fills into a handful — the same picture, a fraction of the calls.

3. Adapt to the frame you are actually getting. Measure frame time over a second; if it is over ~20ms, raise step (or lower the internal width) a notch, and lower it again when there is headroom. Bound it so it cannot oscillate visibly.

And give us a number to argue with. Add a small readout — F3 or a ~ key — showing fps, mean frame ms, snapshot Hz, the interpolation delay in use, and the internal render width. Right now "it feels choppy" and "it delivers 30Hz cleanly" are both true and nobody can tell which part is at fault. Make it print, and put what you measured in the pull.

The bar: this is presentation only — no change to what is sent, predicted or simulated. The batching and the internal-resolution choice are pure functions of the column data and the window size; test them as such, including that a run of equal levels produces one rect and that the picture's column count is unchanged.

commented

Two changes of direction from the person playing it, and they matter more than anything in the issue above.

1. Fidelity is cheap, frame rate is not. His words: "this game can literally be stick figures if that's easier to run." So take that permission and spend it. If flat-shaded columns, one fill per run, a 960px internal buffer and bodies drawn as two lines and a head gets the frame time down, do it and do not apologise for how it looks. The thing being shown off is a Valorant match running in a browser — sites, spike, abilities, credits, the round loop — not the texture work. Keep it unmistakably Valorant in its rules and vocabulary; let the pixels be as simple as they need to be.

2. Enemies pop out of nowhere, and it reads as broken. This one is partly our own doing. The server correctly sends only what you can see, so a body that steps into a sightline arrives with no history — and the interpolation makes it worse, because a body that was out of sight starts fresh at its exact position, ~45–130ms further along than where it entered view. So they do not walk into the room; they materialise mid-stride.

Do not fix this by sending more — the vision rule is the one thing on this app I will not trade. Fix it in the drawing:

  • Fade and scale a body in over ~120ms when it first appears, from slightly small and transparent to full. The eye reads that as "someone came round the corner" rather than "a thing blinked into existence".
  • Leave a mark where someone was last seen for about a second after they drop out of sight — a dimming silhouette or a simple marker, clearly not a live body. It answers "where did they go" and it is information you already had, so it leaks nothing.
  • Enter from the edge where possible: if a body's first sample and its previous known position are both known, begin the fade from the direction it came, so it slides into view instead of appearing at its destination.

The test for all of this is a person watching a bot cross a doorway and not being surprised in the wrong way. Say in the pull what the frame time was before and after, and keep the readout key — the number is the point.

Sign in to join this conversation.

↑↓ navigate ↵ open esc close

Keyboard shortcuts