Kirino 的游戏宝藏
← Back to issue 022
Game observation

Invisible Sixty Frames and a Diff of Zero Bytes: How Esteban Estrada Used Look Sheets to Nail Guildhall's Kingdom-Wide Optimizations as Invisible Wins

If the industry default for running a living kingdom in a browser is often "make it look good first, then when it stutters turn down shadows, cut the far field, let LODs pop in…

Saved articles ↗

If the industry default for running a living kingdom in a browser is often "make it look good first, then when it stutters turn down shadows, cut the far field, let LODs pop in front of the camera, and write 'mobile-optimized' into the press note," then on 18 October 2026 Esteban Estrada's essay *Invisible Wins: 60 FPS for a Whole Kingdom, Even on a Phone*, published on codestz.dev, almost publicly rewrote that bill. The work is Guildhall. It runs in a WebGL browser tab. The target is blunt: hold 60 on an M1 Pro, and stay smooth on a phone. This essay did not long-play the finished product; scenes and numbers below come from the author's public experiment notes and the live demo.

A 12,000-file island becomes 2,813 land tiles and about 30,052 pieces: houses, trees, fences, crops, a castle. On top of that sit crowds with daily routines, a sea with waves, a sky, and a sun that turns through the day and drags every shadow with it. It is not a "if it opens, we won" demo. The line Estrada wrote into the decisions file on 12 October 2026 is harder than any frame-rate slogan: *Performance must not change what you see.*

The through-line image of this piece is the look sheet, and "invisible." It sounds like a QA tool name. As production ethics it is a verifiable promise: make the picture match at the pixel level first, then talk about which cut saved time. What you save must leave a ledger. What changes is cost. The kingdom in the lens stays byte-identical.

Guildhall 12k-file island, heavy end

1. Default logic: optimization equals changing the picture; enough FPS counts as a win

The industry default is clean, and therefore lazy.

Once a 3D web project sinks from desktop to phone, teams usually open a familiar menu: lower device pixel ratio, shorten shadow distance, kill cascades, swap distant meshes for crude shells, halve the "looks expensive" post stack. Players learn the same vocabulary—they say "acceptable," "far field can be soft," "medium on phone is fine." That pact makes visible downgrade forgivable industry grammar.

Guildhall is hard to fit inside that grammar: whole-island scale, day-night shadows that turn with the sun, crowd schedules, weather and show programs, and 60 fps still required inside a browser tab. Its budget aims at "a whole kingdom." Industry instinct points almost automatically at "swap the far look, coarsen shadows, let LOD jump on zoom."

V1 walked into that trap. Zoom in and out and the world visibly swapped: things popped; every distance had a slightly different look. So before any perf commit, Estrada nailed the rule. That one line made the work much harder, and much better. A change only counted if it was invisible. To prove "invisible" you need numbers and pictures, not opinions—including his.

The fork is here. The default asks what look to cut when FPS fails. They ask which implementations must become cheaper while staying byte-identical on screen—and which "sure wins" the measurements should send back.

2. Breaking the script: a lab protocol, and the machine before the scene

Performance is easy to claim and hard to prove. An idea that sounds like a sure win often isn't. Estrada arrived with an experience list: levels of detail, cached shadows, lighter far shadows, macro-shells for far clusters, taking the crowd out of far shadows, culling tiny shadow casters, cheaper water in the distance. Every item is a real technique. A list is only a list of guesses. Some would win, some would cost nothing, some would quietly change what you see.

So before optimizing he wrote the contract and the plan, then handed long measurement and implementation passes to a bigger model, Fable 5.1, running on that plan, and decided what shipped. The contract reads more like a lab protocol than a task: six fixed camera poses—hero, mid, wide, lanterns at 19:30, a busy noon square, low over the sea—at 4,000 and 12,000 files, three runs each, nothing else running. A ledger: every change appends before → after per pose; a gain smaller than measured noise is reverted. Blocking invariants: a look sheet (5 zooms × several hours, before vs after), a zoom sweep with no pops, shadows never stale or jumping. One technique per commit, each with its own numbers. A quiet machine, a real phone, a human checkpoint: no other browsers during a bench; a real-device check; he sees the ledger and look sheets halfway; at the end an agent that did not write the code reruns everything.

What measuring found first was not a slow shader. It was the machine.

Mac GPU timers lie. On ANGLE over Metal, `EXT_disjoint_timer_query_webgl2` reads each command buffer's wall time, takes 20–30 frames to resolve, and costs about 1.3 ms a frame on its own. Timers became opt-in; cost was measured by subtraction. One busy tab triples frame times. A "quiet" bench read every pose at about 34 ms; his own Chrome still had a live Guildhall tab open, GPU process at 9–18% CPU. Closed, the same commit read 9.7 / 12.2 ms (p50 / p95). Nothing had regressed. A phone could beat the laptop: on his Xiaomi the island ran above 60 while the Mac felt slower, because agents' headless browsers shared its GPU. From then on every bench checked `ps` for other browsers and recorded it in the ledger.

That is the kind of rule experience gives you: before you optimise the scene, isolate the machine. Otherwise you are optimising noise.

Look sheet, perf round 3: noon, closest zoom

3. Concrete cuts in the frame budget: instances, shadows, shaders, workers, bytes

Where a frame actually goes, Estrada walks in the order that mattered. Each box is a lesson.

Every static piece goes into one three.js `BatchedMesh`. Geometries share one set of buffers; the island draws with a single multi-draw. A full frame is about 35 draw calls—why WebGPU was not worth switching to yet: draw calls were never the bottleneck. What costs on Metal is instance count more than triangle count. Before every pass, three's batch walks every instance: frustum test, then geometry range into the draw list and upload. With roughly 18,000 instances, that walk is the bill. He replaced it with his own `LodBatchedMesh`: LOD tiers live in the same buffers; instances between tiers are listed twice with a dither code, crossfading in a band of about 2 to 2.8 CSS pixels; far clusters become one shell instance; a still camera keeps its list. Switching LOD off (`?lod=0`) makes the wide view 23–25 ms slower. But it only swaps where the difference is under about a pixel, and it fades.

Shadows follow the sun without walking the island every time. Three cascades: near about 30 m across at about 2.3 cm a texel, middle at 100 m, and one for the whole town. On a still camera none re-render; every quarter degree of sun turn (`SHADOW_TURN = 0.25°`) redraws all three. With the day running that is about 30 cascade re-renders a second. The ledger put an unexpected number on it: the day running cost the hero view about 30 fps (85 → 56 on the laptop profile, 105 → 58 on the phone). A coarser turn would redraw less, but shadows would step visibly—rule says no. Round 4 gave side passes their own buffers so a cascade re-render would not force another ~18k walk and upload on a still camera. Round 5 keeps the town cascade's list (the box holds the whole island from any sun angle; build once until an instance changes) and adds about a 32 × 32 cull grid; the walk stays in instance order, so the list is the same list in the same order, and a test proves it. Day-running CPU p95 (CPU ×4 throttle): laptop 22.4 → 19.8 ms (round 4), hero 19.3 → 16.2 (round 5), phone hero 18.5 → 16.2; every day-running row in round 5 dropped 2.3 to 3.4 ms. He is also honest about what this did not do: a frame that turns the sun still draws three cascades on the GPU, so the wide view on the laptop profile did not move.

The hitches were not where he expected either. Every frame over 25 ms traced to shader programs linking at first use. On ANGLE over Metal a typical program waits 30–50 ms; heavy ones hundreds. The parallel-compile extension can report ready before Metal finishes. A measured 2 s grace period still left a 35 ms hitch. So every program the world will ever need is linked under the loading cover; show programs are pinned; the batch warm-up now uses one-triangle stand-ins with real attributes. On the 12k island, frames over 25 ms fell from 10 to 1; programs compiled before reveal went from 40 to 32.

The world is built in a worker. In round 2 the life plan moved into a worker too—same plan byte for byte, proven by test. Longest main-thread task after the first frame: 4,000 files 1,999 → 323 ms; 12,000 files 3,767 → 495 ms. Then a ~2.4 s stall remained that was neither CPU work nor shader compile: the batch was drawn while geometry was still being copied in slices; each `bufferSubData` into a buffer the previous frame was still drawing made ANGLE/Metal copy the whole ~130 MB again. The fix was almost boring: keep the batch hidden until every geometry is in, then upload once. Batch build 4.8–5.1 → 1.1–1.4 s; whole island 10.5 → 4.5 s. That one was on nobody's list. It came from reading the trace and asking why a GL call would wait on a GPU that had nothing to do.

On the wire, Vercel compresses text and JS but sends `.glb` as-is. The character rig alone was 3.14 MB. The build ships every library GLB gzipped; the page inflates with `DecompressionStream`, deciding by bytes `1f 8b`, not by file name. First load of a town: 9.25 → 5.24 MB transferred; GLBs 7.89 → 4.02; character rig 3.14 → 0.86; requests 412 → 311.

The last pass of round 2 was small separate commits, each measured: dark lamps free by day, one shared palette material per object kind, no double anti-aliasing with SMAA, less per-frame garbage. Uncapped 4k hero p50/p95 8.9 / 11.7 → 6.3 / 8.1; 12k hero 9.0 / 12.4 → 6.3 / 8.3. Frames dropped by roughly 25–35%. Same pictures.

Round 3 was the first seconds. Before, opening a town flashed a bare pale canvas under the Studio chrome; now an inline boot cover paints from the first frame and fades into the world's own cover—no blank frame after first paint in the captures. The life worker starts planning at about 1 s so the plan lands while the batch is still building. Cold loads, vsync on, median of three: 4k reveal 4.9 → 3.7 s; 12k 7.1 → 5.1 s. Six hundred milliseconds of every reveal is the cover's own fade; he left it, because the fade is part of the moment.

About 504 ms after navigation: bare canvas vs boot cover

4. Pacing before the quality ladder, and an honest table of nos

The last lesson about "smooth" is that pacing comes before any quality drop. A rate jumping between 60, 40 and 30 feels worse than a steady 30. The stage draws on every n-th vsync; skipped vsyncs draw nothing and run no hook; every animation runs on real elapsed time, so nothing slows at 30. Auto steps down after two bad seconds in a row, and up only after eight calm ones; if a raise does not hold, the next wait doubles. Settings → Motion offers Auto, Smooth 30 or Max. On an emulated phone (390 × 844, DPR 3, CPU ×6), frames went from 33.4 / 41.0 ms to 16.7 / 18.4 ms: a steady 60 on a 120 Hz panel. Only when even the 30 cadence cannot hold does the quality ladder step down, one rung at a time—`TIERS` from full look (dprFloor 1.5, shadowMap 2048, lodBias 1, particles 256) to the lowest (0.75 / 1024 / 2.5 / 64). Phones start one tier down, weak GPUs two; Auto climbs back after 20 calm seconds. On his M1 the tiers barely move the frame—the island is GPU-bound there whatever the shadow resolution—which is exactly why the ladder sits behind pacing.

Every round ended the same way. A look sheet: same island, same frozen clock, 5 zoom levels at noon, 16:30 and 19:30, captured before and after, diffed pixel by pixel. For round 3, all 15 shots matched with a mean difference of 0.000/255: byte for byte. In rounds 4 and 5 the only differences were one flat piece at the closest zoom that comes and goes between two captures of the same build. A moving-sun strip of 48 frames was byte-identical too. That is the bar: if the picture moved, the optimisation did not ship.

The part of the ledgers he likes most is the honest negatives. Many "sure wins" on the round-4 list measured otherwise: cull shadow casters smaller than a texel—almost nothing is sub-texel where shadows cost; at two texels a visible dot of shadow would vanish—not built; take the crowd out of far cascades—already so, nothing to do; skip water under the island / cheaper far water—built and measured twice, wide and hero unchanged; a real far-water saving would fade waves earlier—a look change—dropped; lighter far-cascade softening, spreading cascade re-renders, redrawing shadows less often as the sun turns—same cost or visible—not built or rejected. One thing he accepted: a 12,000-file island from the widest view is still heavy on the laptop profile. The levers left all touch the look. His note: "12k is too big and crazy." Demo-size islands are smooth; he would rather say that than ship a blurrier kingdom.

The series theme closes clearly here: experience defines the problem, the rules and the acceptance; AI executes fast. Nearly all of the typing was the model. He wrote the contract and the plan, delegated the long passes, and decided what shipped. Execution was fast because the rules were clear. What came from experience: the invisible-perf rule before the first commit; isolating the machine before trusting a number; reading a trace and asking why a buffer copy would wait on an idle GPU; letting the look sheet overrule his own ideas when it said no.

Closing: independence means paying for "invisible" acceptance

Put this 18 October note back into the autumn 2026 browser-game floor and what it hits first is a quieter production habit: people default to optimization-as-changing-the-picture, default to FPS slogans over pixel evidence, default to "it runs now" as enough, and rarely ask whether the kingdom after the fix is still the same kingdom. Estrada opens another path. The contract can be as plain as six fixed poses and three repeats. Acceptance can be as hard as mean diff 0.000/255. A negatives table can publicly nail "sure wins." AI can be fast; a human must decide what ships.

For small teams the material is especially useful. You may not have 18k-instance BatchedMesh or Fable 5.1. You do have your own "already good enough" list: an optimization that only watches average frames, not a quiet machine; an LOD that pops on zoom written off as "players won't notice"; shadows that step for FPS; a warm-up that compiled a variant nothing ever drew; a "save water" change that measured identical before/after yet still wanted to stay in main as a victory. Guildhall's public work writes the cost in plain dirt. A look sheet can go black to zero. A ledger can be one technique per line. What is expensive is paying the human checkpoint before merge—and admitting in the decisions file that 12k from the widest view is still too crazy.

Short actionable tips:

  • Before the first perf commit, write a veto rule: which looks, once deformed, mean the optimization did not win; put "invisible" in the decisions file, not in after-the-fact excuses.
  • Fixed poses, repeats, quiet machine, real device—land at least one full set; revert gains smaller than noise; do not declare victory by feel.
  • One technique per commit, each with before → after; for culls and softenings that might move the picture, run look sheets or byte diffs first—black, then merge.
  • Pacing before the quality ladder: rather a steady 30 than flapping between 60 and 30; move the ladder only when 30 itself cannot hold, and record which "sure wins" it vetoed.

Twelve thousand files, about thirty thousand pieces, thirty-five draw calls, a diff of 0.000, and one line—"12k is too big and crazy." While the demo still runs and the code and ledgers stay public, the maker has already fixed the audit standard: not "can it hit sixty," but "after it hits sixty, is what you see still the same kingdom."

小光

Sources and further reading

Esteban Estrada (2026-10-18) Invisible Wins: 60 FPS for a Whole Kingdom, Even on a Phone

Esteban Estrada (2026-10-18) Feet That Don't Skate: Why 'It Looks Right' Is the Hardest Requirement

Live demo https://guildhall.codestz.dev

Sign in to keep your reading history.

View your likes and saved articles across devices.

Sign in