You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #839 — [Audit][Round 2] A++ umbrella. Phase: P1 · Finding: R2-1 · Risk: HIGH
Problem
LODManager is the worst God object in the repo. 2,715 LOC, 2,207 impl LOC, 89 functions, and 10–12 distinct concerns merged into a single struct. Round 1 split lod_mesh.zig (#785) and the leaves (lod_geometry, lod_ingest, lod_scheduler, lod_cache, lod_store, lod_streaming_coordinator) are exemplary — but the orchestrator itself absorbed the residual complexity.
This issue supersedes any informal follow-up to #785 and is the single highest-leverage refactor in Round 2.
Evidence
Struct fields group into ≥10 concerns — modules/world-lod/src/lod_manager.zig:183-289:
update() orchestrates 14 subsystems in 110 lines — lod_manager.zig:812-922. Textbook "feature envy over its own subsystems": mesh deletion sweep → NaN safety → player position publish → queue reprioritization → throttling → coverage unload → cache recenter → scheduling (×N levels) → generation dispatch → state transitions → uploads → stats logging → memory budget → distance unload → ingestion drain → edit flush → store flush → transition decay.
Locking is smeared: 87 acquisitions of self.mutex across ~25 functions including updateStats, queueLODRegions, processQueuedGenerations, processStateTransitions, processUploadsWithBudget, unloadDistantForLevel, enforceMemoryBudget, decayTransitionFrames, markRegionRenderable, recordCacheHit, recordCacheMiss, applyIngestionToRegions, cacheEnabled, logLegacyCacheNotice, cacheDirPathSnapshot. The inline comment at line 276 — "never held while acquiring mutex (avoids cross-lock deadlocks)" — is a clear admission that locking discipline has outgrown ad-hoc reasoning.
processLODJob worker callback spans 6 subsystems in ~150 lines — lod_manager.zig:2055-2203: state lookup/validation, stale-job detection, token check, pinning, lock drop, simplified-data allocation, heightmap generation via generator.generateHeightmapOnly, abort-flag polling, mesh building via buildMeshForChunk, state transition.
Duplicated coverage logic — LODManager.areAllChunksLoaded (lod_manager.zig:1741-1766) and LODRenderer.isCoveredByChunks (lod_renderer.zig:520-576) both compute "what chunks does this LOD region touch and are they loaded?" with subtly different policies, and both define the same CHUNK_COVERAGE_PADDING = 1 constant.
Fix
Extract 5 subsystems (priority order), each landing as one focused PR against this issue. After all 5 land, LODManager becomes a thin orchestrator (~400–500 LOC).
Owns: deletion_queue, deletion_timer, queueMeshDeletion, processMeshDeletions. Manager just calls disposal.queue(mesh) and disposal.tick(dt, MAX_SWEEP). Removes 2 fields + 2 methods. Kills the "manager knows how to wait for GPU idle" smell.
Extraction 4 — LODMemoryGovernor
Owns: memory_used_bytes, radius_shrink_chunks, enforceMemoryBudget, regionMemoryBytes.
Public interface: track(bytes), enforce(regions, meshes, player_pos) Decision, currentPressure() Pressure. Removes 2 fields + 2 methods + ~100 LOC. Pushes eviction policy out of the orchestrator.
Extraction 5 — LODJobDispatcher
Owns: gen_queues, lod_gen_pool, next_job_token, processLODJob, processQueuedGenerations, processStateTransitions, processUploads, requeueUpload.
Public interface: tick(ctx), pushGeneration, pushMeshing, pushUpload. Manager holds an opaque handle. Removes ~5 fields + 6 methods + ~400 LOC. Biggest LOC win, most delicate — locking must move with it.
Final — collapse update() to ~30 lines
After extractions 1–5, update() becomes a thin delegating loop: cacheStore.tick(); ingestion.tick(); disposal.tick(dt); memoryGov.track(currentBytes); dispatcher.tick(ctx); decayTransitions();. Target: ~30 LOC.
Bonus — extract LODCoverageQuery
Single source of truth for "are all chunks under this region loaded?" — eliminates the duplicated CHUNK_COVERAGE_PADDING constant and the two divergent implementations between manager (1741-1766) and renderer (520-576).
Verification
nix develop --command zig build test (includes shader validation)
Required: headless screenshot baseline before extraction 1 and after each extraction (-Dskip-present, low/medium/high presets) — visual parity must be bit-exact
nix develop --command zig build test -- --test-filter "LOD" after each extraction
508 LOC of tests in lod_manager.zig must continue to pass; tests move with their code into the new modules
nix develop --command zig build -Doptimize=ReleaseFast (perf check — extraction must not regress frame times)
Constraints
One extraction per PR — do not bundle. Each PR is independently revertable.
Preserve current behavior bit-for-bit (visual + perf). Extraction is purely structural.
Locking moves with the data: when extracting LODIngestionQueue, the ingestion_mutexmust move into the new struct; do not leave the manager reaching across to it.
Keep worker-thread RHI isolation intact — extracted schedulers/dispatchers must not introduce RHI calls on workers.
Conventional commits: refactor(world-lod): extract LODCacheStore from LODManager etc.
Part of #839 — [Audit][Round 2] A++ umbrella.
Phase: P1 · Finding: R2-1 · Risk: HIGH
Problem
LODManageris the worst God object in the repo. 2,715 LOC, 2,207 impl LOC, 89 functions, and 10–12 distinct concerns merged into a single struct. Round 1 splitlod_mesh.zig(#785) and the leaves (lod_geometry,lod_ingest,lod_scheduler,lod_cache,lod_store,lod_streaming_coordinator) are exemplary — but the orchestrator itself absorbed the residual complexity.This issue supersedes any informal follow-up to #785 and is the single highest-leverage refactor in Round 2.
Evidence
Struct fields group into ≥10 concerns —
modules/world-lod/src/lod_manager.zig:183-289:regions[LODLevel.count]RegionMapmeshes[LODLevel.count]MeshMapgen_queues,lod_gen_pool,next_job_tokenupload_queuescache_dir_path,store_mutex, 12+ private fnspending_ingestions,ingestion_mutex, 9 methodsmemory_used_bytes,radius_shrink_chunksdeletion_queue,deletion_timertransition_queuemutex(RwLock),store_mutex,ingestion_mutex,stop_flag,player_cx/czupdate()orchestrates 14 subsystems in 110 lines —lod_manager.zig:812-922. Textbook "feature envy over its own subsystems": mesh deletion sweep → NaN safety → player position publish → queue reprioritization → throttling → coverage unload → cache recenter → scheduling (×N levels) → generation dispatch → state transitions → uploads → stats logging → memory budget → distance unload → ingestion drain → edit flush → store flush → transition decay.Locking is smeared: 87 acquisitions of
self.mutexacross ~25 functions includingupdateStats,queueLODRegions,processQueuedGenerations,processStateTransitions,processUploadsWithBudget,unloadDistantForLevel,enforceMemoryBudget,decayTransitionFrames,markRegionRenderable,recordCacheHit,recordCacheMiss,applyIngestionToRegions,cacheEnabled,logLegacyCacheNotice,cacheDirPathSnapshot. The inline comment at line 276 — "never held while acquiringmutex(avoids cross-lock deadlocks)" — is a clear admission that locking discipline has outgrown ad-hoc reasoning.processLODJobworker callback spans 6 subsystems in ~150 lines —lod_manager.zig:2055-2203: state lookup/validation, stale-job detection, token check, pinning, lock drop, simplified-data allocation, heightmap generation viagenerator.generateHeightmapOnly, abort-flag polling, mesh building viabuildMeshForChunk, state transition.Duplicated coverage logic —
LODManager.areAllChunksLoaded(lod_manager.zig:1741-1766) andLODRenderer.isCoveredByChunks(lod_renderer.zig:520-576) both compute "what chunks does this LOD region touch and are they loaded?" with subtly different policies, and both define the sameCHUNK_COVERAGE_PADDING = 1constant.Fix
Extract 5 subsystems (priority order), each landing as one focused PR against this issue. After all 5 land,
LODManagerbecomes a thin orchestrator (~400–500 LOC).Extraction 1 —
LODCacheStoreOwns:
cache_dir_path,logged_legacy_cache_notice,store_mutex, and all ofcacheKey/legacyCacheFilePath/readStorePayload/writeStorePayload/deleteStorePayload/deleteStoreContainer/loadCachedSourceData/saveCachedSourceData/recordCacheHit/recordCacheMiss/enableCache/flushDirtyStores/logLegacyCacheNotice/cacheDirPathSnapshot/cacheEnabled.Public interface:
enable(path),load(key) ?Data,save(key, data),flush(regions_iter),validateHeader() bool.Removes ~12 private fns + 3 fields + ~250 LOC.
Extraction 2 —
LODIngestionQueueOwns:
pending_ingestions,edit_dirty,ingestion_mutex,chunk_resolver,edit_cooldown,ingestion_drain_per_frame.Public interface:
ingestChunk(cx, cz, chunk, prov),markEdited(cx, cz),requestDeferred(cx, cz, prov),drain(resolver, regions),flushEdits(resolver, regions),tick().Removes 9 methods + 6 fields + ~250 LOC + 1 mutex + the cross-lock ordering rule. Highest-impact single extraction.
Extraction 3 —
LODMeshDisposalQueueOwns:
deletion_queue,deletion_timer,queueMeshDeletion,processMeshDeletions. Manager just callsdisposal.queue(mesh)anddisposal.tick(dt, MAX_SWEEP).Removes 2 fields + 2 methods. Kills the "manager knows how to wait for GPU idle" smell.
Extraction 4 —
LODMemoryGovernorOwns:
memory_used_bytes,radius_shrink_chunks,enforceMemoryBudget,regionMemoryBytes.Public interface:
track(bytes),enforce(regions, meshes, player_pos) Decision,currentPressure() Pressure.Removes 2 fields + 2 methods + ~100 LOC. Pushes eviction policy out of the orchestrator.
Extraction 5 —
LODJobDispatcherOwns:
gen_queues,lod_gen_pool,next_job_token,processLODJob,processQueuedGenerations,processStateTransitions,processUploads,requeueUpload.Public interface:
tick(ctx),pushGeneration,pushMeshing,pushUpload. Manager holds an opaque handle.Removes ~5 fields + 6 methods + ~400 LOC. Biggest LOC win, most delicate — locking must move with it.
Final — collapse
update()to ~30 linesAfter extractions 1–5,
update()becomes a thin delegating loop:cacheStore.tick(); ingestion.tick(); disposal.tick(dt); memoryGov.track(currentBytes); dispatcher.tick(ctx); decayTransitions();. Target: ~30 LOC.Bonus — extract
LODCoverageQuerySingle source of truth for "are all chunks under this region loaded?" — eliminates the duplicated
CHUNK_COVERAGE_PADDINGconstant and the two divergent implementations between manager (1741-1766) and renderer (520-576).Verification
nix develop --command zig build test(includes shader validation)-Dskip-present, low/medium/high presets) — visual parity must be bit-exactnix develop --command zig build test -- --test-filter "LOD"after each extractionlod_manager.zigmust continue to pass; tests move with their code into the new modulesnix develop --command zig build -Doptimize=ReleaseFast(perf check — extraction must not regress frame times)Constraints
LODIngestionQueue, theingestion_mutexmust move into the new struct; do not leave the manager reaching across to it.refactor(world-lod): extract LODCacheStore from LODManageretc.Notes
LODGPUBridge/LODRenderInterface) is done and clean —LODManagerdoes not importengine-rhi. The remaining debt is data-structure encapsulation and pulling 4 subsystems out.LODMesh/LODChunkencapsulation) for clean extraction boundaries — consider landing R2-2 first.world-lod— the rot is structural, not attention-driven.Tracking: #839