Scale defense-post border + fill rendering to thousands of posts (#4181)

## Description

Scales the defense-post border effect so it works with **thousands** of
Defense Posts instead of silently capping at 64.

### Problem
The border "checkerboard" (drawn on a player's border tiles when a
same-owner Defense Post is within range) was computed per-pixel: for
every border fragment, the shader looped over a `uniform vec4
uDefensePosts[64]` array doing a distance test. Two issues:
- **Hard cap of 64** — posts beyond the first 64 were dropped, so their
checkerboard never appeared.
- **Wrong cost shape** — work was `border_tiles × posts`; every added
post made every border pixel slower.

### Solution: invert the loop into a coverage texture
New `DefenseCoveragePass` stamps one instanced circle per post into a
map-resolution `R8` coverage texture (`1.0` = tile is within range of a
**same-owner** post; the owner check samples `tileTex` at stamp time, so
enemy posts never light up your border). It's a single
`drawArraysInstanced` regardless of post count — the same instancing
pattern `UnitPass`/`StructurePass` already use. The border-stamp shader
now reads one texel of that texture instead of looping; the old uniform
array, the 64-cap, and the per-fragment scan are removed from
`border-compute`/`BorderStampPass`/`BorderScatterPass`.

### Incremental re-stamping (dirty-block grid)
Coverage depends on tile ownership, which drips every frame during
combat, so a full re-stamp every frame would be wasteful at high post
counts. Because a tile changing owner only changes *its own* coverage,
the pass tracks a grid of dirty **blocks** and re-stamps only the blocks
containing changed tiles, scissored to each block (`gl.scissor` confines
the clear + draw to the changed region). Post add/remove and full tile
uploads fall back to a whole-map stamp; so does a frame where most
blocks are dirty. Per-frame cost tracks *how much changed*, not *how
many posts exist*, and scattered fronts (e.g. opposite corners) become
independent small block draws.

### Territory-fill darkening
The coverage texture marks every same-owner in-range tile (interior
included, not just borders), so `TerritoryPass` now also samples it to
darken the territory **fill** around posts. New tunable
`mapOverlay.territoryDefenseDarken` (live-editable in the graphics debug
GUI alongside `defenseCheckerDarken`).

### Performance
Tested with ~1,000 posts blanketing a map — smooth, including on a
low-end (~10-year-old) Chromebook.

## Files
- **New:** `passes/DefenseCoveragePass.ts`,
`shaders/defense-coverage/defense-coverage.{vert,frag}.glsl`
- **Edited:** `Renderer.ts`, `BorderStampPass.ts`,
`BorderComputePass.ts`, `BorderScatterPass.ts`, `TerritoryPass.ts`,
`border-stamp.frag.glsl`, `border-compute.frag.glsl`,
`territory.frag.glsl`, `RenderSettings.ts`, `render-settings.json`,
`debug/Layout.ts`

## Notes
- No user-facing text (no `translateText`/`en.json` changes needed).
- No `src/core` changes — purely client rendering, so no simulation
tests; verified via `tsc`, ESLint, `build-prod`, and in-game.
This commit is contained in:
Evan
2026-06-08 10:18:02 -07:00
committed by GitHub
parent 8a510977ba
commit 26d8a314ae
14 changed files with 442 additions and 98 deletions
@@ -66,6 +66,7 @@
"mapOverlay": {
"trailAlpha": 0.588,
"defenseCheckerDarken": 0.7,
"territoryDefenseDarken": 0.85,
"staleNukeBase": 0,
"staleNukeVariation": 0.05,
"staleNukeAlpha": 1,