perf(sky): ~10x cheaper night-sky lattice — one-round cell hash, 2x2x2 block, bit-sliced stars
Some checks failed
CI / linux-portable (push) Failing after 1m37s
CI / windows-gate (push) Successful in 6m35s
CI / release (push) Has been skipped

The owner measured 63% GPU at night vs 28% by day: the starfield
lattice was the whole difference. Three lossless cuts:
- one packed-multiply hash round per cell instead of ~9 chained PCG
  rounds (position/presence bit-sliced from one result, brightness/tint
  from a second);
- the 3x3x3 neighbourhood sweep becomes the 2x2x2 block around the
  sample — a star's visible footprint (a few px) is far smaller than a
  lattice cell on screen, so any star outside that block is over a full
  cell (>=15 px) away and contributes nothing;
- background mottle drops its invisible third octave.

Same densities, sizes, colours, and look; star positions reshuffle
(procedural layout, not authored). sky.frag.spv re-pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-23 18:31:39 +02:00
parent 9fcc3f4870
commit 9cf15e1f13
4 changed files with 43 additions and 31 deletions

View file

@ -88,22 +88,33 @@ vec3 nsTint(float r)
return vec3(0.70, 0.82, 1.00); // hot blue
}
// One-round packed 3D lattice hash (three odd-constant multiplies + a
// single PCG finalise). The 2026-08-23 night GPU measurement (63% night vs
// 28% day at the owner's card) showed the first version's ~9 chained PCG
// rounds per cell were the whole bill; one round is statistically ample for
// star placement.
uint nsCellHash(ivec3 c, uint seed)
{
uint h = uint(c.x + 1024) * 0x8DA6B343u
^ uint(c.y + 1024) * 0xD8163841u
^ uint(c.z + 1024) * 0xCB1AB31Fu
^ seed;
return nsPcg(h);
}
float nsVnoise(vec3 p, uint seed)
{
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
ivec3 ii = ivec3(i);
float acc = 0.0;
for (int c = 0; c < 8; ++c)
{
vec3 o = vec3(float(c & 1), float((c >> 1) & 1), float((c >> 2) & 1));
vec3 q = i + o;
uint h = nsPcg(seed
^ nsPcg(uint(int(q.x) + 512)
^ nsPcg(uint(int(q.y) + 512)
^ uint(int(q.z) + 512))));
vec3 w = mix(1.0 - f, f, o);
acc += nsRand(h) * w.x * w.y * w.z;
ivec3 o = ivec3(c & 1, (c >> 1) & 1, (c >> 2) & 1);
float v = nsRand(nsCellHash(ii + o, seed));
vec3 wf = mix(1.0 - f, f, vec3(o)); // trilinear weights
acc += v * wf.x * wf.y * wf.z;
}
return acc;
}
@ -116,7 +127,6 @@ vec3 nsStarTier(vec3 dir, vec3 ex, vec3 ey, uint seed, float cells,
// seam artefacts by construction (the 2026-08-23 gate screenshot's
// glowing "Y" was cube-face fwidth blowup — do not bring faces back).
vec3 g = dir * cells;
vec3 cellF = floor(g);
// Exact tangent-plane -> screen-pixel solve: 2x2 normal equations of
// the direction derivatives. Stars come out perfectly ROUND and sized
@ -127,32 +137,35 @@ vec3 nsStarTier(vec3 dir, vec3 ex, vec3 ey, uint seed, float cells,
float c = dot(ey, ey);
float det = max(a * c - b * b, 1e-14);
// A star's visible footprint (a few px) is far smaller than a lattice
// cell on screen, so only the 2x2x2 block around the sample can hold a
// star that contributes — the 3x3x3 sweep was pure waste (GPU bill,
// 2026-08-23): any star outside this block is over a full cell away.
ivec3 base = ivec3(floor(g - 0.5));
vec3 acc = vec3(0.0);
for (int dz = -1; dz <= 1; ++dz)
for (int dy = -1; dy <= 1; ++dy)
for (int dx = -1; dx <= 1; ++dx)
for (int ci = 0; ci < 8; ++ci)
{
vec3 cc = cellF + vec3(float(dx), float(dy), float(dz));
uint h = nsPcg(seed
^ nsPcg(uint(int(cc.x) + 512)
^ nsPcg(uint(int(cc.y) + 512)
^ nsPcg(uint(int(cc.z) + 512) ^ uint(cells)))));
if (nsRand(h) > density) continue;
uint h2 = nsPcg(h);
uint h3 = nsPcg(h2);
uint h4 = nsPcg(h3);
uint h5 = nsPcg(h4);
uint h6 = nsPcg(h5);
vec3 sdir = normalize(cc + vec3(nsRand(h2), nsRand(h3), nsRand(h4)));
ivec3 cc = base + ivec3(ci & 1, (ci >> 1) & 1, (ci >> 2) & 1);
uint h = nsCellHash(cc, seed ^ uint(cells));
// Bit-slice ONE hash for presence + position; one more round for
// brightness + tint. (Was five chained rounds per star.)
if (float(h & 0xFFu) > density * 255.0) continue;
vec3 jitter = vec3(
float((h >> 8) & 0xFFu),
float((h >> 16) & 0xFFu),
float((h >> 24) & 0xFFu)) * (1.0 / 255.0);
vec3 sdir = normalize(vec3(cc) + jitter);
vec3 v = sdir - dir * dot(sdir, dir); // tangent-plane offset
float bx = dot(v, ex);
float by = dot(v, ey);
vec2 sPx = vec2(bx * c - by * b, by * a - bx * b) / det;
float d2 = dot(sPx, sPx);
if (d2 > 400.0) continue; // > 20 px: contributes nothing
float t = nsRand(h5);
uint h2 = nsPcg(h);
float t = float(h2 & 0xFFFFu) * (1.0 / 65535.0);
float br = mix(bMin, bMax, t * t * t);
vec3 tint = nsTint(nsRand(h6));
vec3 tint = nsTint(float((h2 >> 16) & 0xFFu) * (1.0 / 255.0));
// Round gaussian core; optional soft round halo on the bright tiers.
// NO diffraction spikes / lens flare — user-directed 2026-08-23:
// spikes are a photographic artefact, not a naked-eye sky.
@ -169,9 +182,8 @@ vec3 nightSky(vec3 dir, uint seed)
vec3 ey = dFdy(dir);
// Faint cool background mottle (0.4%..1.3% - the user-approved level).
float n = 0.55 * nsVnoise(dir * 3.0, seed ^ 0x9E3779B9u)
+ 0.30 * nsVnoise(dir * 7.0, seed ^ 0x85EBCA6Bu)
+ 0.15 * nsVnoise(dir * 15.0, seed ^ 0xC2B2AE35u);
float n = 0.62 * nsVnoise(dir * 3.0, seed ^ 0x9E3779B9u)
+ 0.38 * nsVnoise(dir * 7.0, seed ^ 0x85EBCA6Bu);
vec3 rgb = (0.004 + 0.009 * n) * vec3(0.85, 0.92, 1.10);
// The star carpet: three density tiers plus sparse bright standouts,

View file

@ -316,7 +316,7 @@
},
{
"stage": "frag",
"sourceSha256": "e7ccdb4d5fece2ceeb48eb43aaa827424df2b68614987eeb3dfb7880dff8ea83",
"sourceSha256": "5785633bc936fad68c97247f6ef8a22e5bcde0fe6d74a2f4676b844a2d7600c5",
"compiled": true
}
]

View file

@ -52,7 +52,7 @@ public sealed class VulkanShaderManifestTests
// override, retail's GameSky::Draw @0x00506FF0 rule (see
// SkyFogRuleTests). A deliberate default-path change, reviewed
// with the world-fog-range fix in the same commit.
["sky.frag.spv"] = "e307a7f3cf84aada86db7c2859c55841a650b46732f12149986562cef0a90ca8",
["sky.frag.spv"] = "1c4ae77056837cbdc188f8cfcc4b0e8851647cdfaf398f25d8c8ff489ef84d57",
["sky.vert.spv"] = "3b51945fa4ff1be1604144df92866bdd47aade22f9dd90267591ef36adb28cde",
["terrain_modern.frag.spv"] = "7b3cdb01b837ed77ee20559a81c1ce5c9d5395300efcc072560ab0be3c5a1af9",
["terrain_modern.vert.spv"] = "9f4cb221ea6aed94a8d23af6cb8e3f3ed96c3cce6e50d135a72d3b55667b1557",