fix(render): shader reserved-word + defensive SkyRenderer dat reads

Two runtime blockers discovered after merging the sky/weather/lighting
branch:

1. GLSL reserved word: mesh.frag + mesh_instanced.frag used \`int active\`
   as a local. On GLSL ES / some drivers \`active\` is a reserved identifier
   and compile fails hard (\"ERROR: 0:38: 'active' : Reserved word\").
   Renamed to \`activeLights\`.

2. SkyRenderer.EnsureMeshUploaded called DatCollection.Get<GfxObj>
   without the _datLock that wraps the streaming pipeline's dat reads.
   DatBinReader has shared buffer state; concurrent reads race and
   throw ArgumentOutOfRangeException from Vec2Duv.Unpack deep in the
   mesh parse. Wrapped both Get<GfxObj> and GfxObjMesh.Build in
   try/catch and cache a null entry on failure so we don't retry every
   frame and crash the render loop. Full fix would plumb _datLock into
   the sky renderer, left as a TODO.

Client now stable end-to-end — in-world, spawn stream flowing,
animation + audio + sky + light UBO all live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-19 11:00:34 +02:00
parent 48b5e1f1b1
commit 187226f504
3 changed files with 24 additions and 6 deletions

View file

@ -52,9 +52,9 @@ layout(std140, binding = 1) uniform SceneLighting {
// light" look relies on crisp boundaries.
vec3 accumulateLights(vec3 N, vec3 worldPos) {
vec3 lit = uCellAmbient.xyz;
int active = int(uCellAmbient.w);
int activeLights = int(uCellAmbient.w);
for (int i = 0; i < 8; ++i) {
if (i >= active) break;
if (i >= activeLights) break;
int kind = int(uLights[i].posAndKind.w);
vec3 Lcol = uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w;

View file

@ -35,9 +35,9 @@ layout(std140, binding = 1) uniform SceneLighting {
vec3 accumulateLights(vec3 N, vec3 worldPos) {
vec3 lit = uCellAmbient.xyz;
int active = int(uCellAmbient.w);
int activeLights = int(uCellAmbient.w);
for (int i = 0; i < 8; ++i) {
if (i >= active) break;
if (i >= activeLights) break;
int kind = int(uLights[i].posAndKind.w);
vec3 Lcol = uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w;

View file

@ -217,14 +217,32 @@ public sealed unsafe class SkyRenderer : IDisposable
{
if (_gpuByGfxObj.ContainsKey(gfxObjId)) return;
var gfx = _dats.Get<GfxObj>(gfxObjId);
// DatCollection isn't thread-safe and the streaming loader can be
// actively reading a shared DatBinReader buffer; sky meshes are
// loaded on the render thread but GfxObj.Unpack can race with the
// streamer. Cache a null entry on any read failure so we don't
// retry every frame and crash the render loop. A future
// refactor should move all dat access behind the _datLock.
GfxObj? gfx = null;
try { gfx = _dats.Get<GfxObj>(gfxObjId); }
catch { gfx = null; }
if (gfx is null)
{
_gpuByGfxObj[gfxObjId] = new List<SubMeshGpu>();
return;
}
var subMeshes = GfxObjMesh.Build(gfx, _dats);
System.Collections.Generic.IReadOnlyList<GfxObjSubMesh>? subMeshes = null;
try { subMeshes = GfxObjMesh.Build(gfx, _dats); }
catch { subMeshes = null; }
if (subMeshes is null)
{
_gpuByGfxObj[gfxObjId] = new List<SubMeshGpu>();
return;
}
var gpuList = new List<SubMeshGpu>(subMeshes.Count);
foreach (var sm in subMeshes)
gpuList.Add(UploadSubMesh(sm));