Commit Graph

23 Commits

Author SHA1 Message Date
Fr0zka 826a8c99dc fix: first-green-build follow-ups (diff-layer assertion + Maze float rounding)
The build passed and six tests ran; five green. Details in OPSTACK-PROGRESS.md.

1. DiffLayerContention's failure was the test, not the plugin.
   GetTotalModificationCount() sums STORED ENTRIES, not operations -- a stroke is
   filed under every chunk its AABB overlaps, so 400 radius-6 spheres straddling
   chunk corners store 3200 entries. The assertion now compares the stored count
   against the chunk fan-out ApplyModification itself returned, which also checks
   that the re-mesh list handed to the caller describes what was actually written.
   Everything the test exists for had already passed: 7 readers, 28.7M read
   rounds against 760 writes and 6 Clear()s, no crash, monotonic version.

2. MazeEquivalence: 454/20000 samples differed by at most 1.907e-06 -- exactly
   one ULP at magnitude 16 -- with ZERO crossing the isosurface, i.e. not one
   triangle would move. Leading hypothesis: the original routes noise coords
   through an FVector (double in UE5) and back to float, rounding twice, while
   the op passed floats straight through; under /fp:fast those round differently.
   The op now reproduces the detour on purpose, with a comment against
   "simplifying" it. Unverified -- it predicts 0 differences next run. If drift
   remains, next candidate is FMA contraction across translation units.

Also recorded, because it is the perf half of the whole refactor: the Maze op
stack proved 23 of 60 tiles uniform. ClassifyTile proves ZERO for any cave
archetype today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:40:51 +02:00
Fr0zka 62e3d5a933 fix(build): FVoxelOpStack is move-only, so it must not be a dllexported class
MSVC C2280 on TArray<TUniquePtr<IVoxelDensityOp>>'s copy path. Putting
VOXELFORGE_API on the class forces the compiler to instantiate every implicit
member, including copy-assignment -- which cannot exist for a move-only element
type. The export moves to AppendStructuralPost, the only out-of-line method.

Also declares the move-only-ness explicitly rather than leaving it implied. That
is the right semantics independently of the compiler: a stack uniquely OWNS its
operators, and copying one would mean cloning polymorphic ops, which is
meaningless here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:36:08 +02:00
Fr0zka 8a33bcb42a docs: CODEMAP rows + progress entry for the Phase 1 Maze port
CODEMAP gains 3.2c (VoxelDensityPrimitives), 3.2d (the operator stack and its
factories), FVoxelOpSample under 3.2b, and the two new tests under 3.12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:33:00 +02:00
Fr0zka 4c53d3bbed feat: Phase 1 — Maze ported to the operator stack, OFF the hot path
Maze decomposes into seven ops with no contortion:

  ConstantRockSource -> LatticeCorridorSource -> SdfRoughnessMod -> SdfCarve
  -> OriginSpine -> BoundarySeal -> PassageCarve

That is the answer to Phase 1's actual question (OPSTACK-PLAN section 4's
stop-trigger: "does the source/modifier split fall out naturally?"). It does.
Three of those ops are already shared: ConstantRockSource is the first line of
TunnelNetwork, Maze AND VerticalShafts; SdfCarve is the same six lines in all
three; the structural post is identical across all six density functions.

GetDensityAt and ClassifyTile are NOT touched. The archetype switch is still the
only path feeding the game, so nothing in a running world can change. The port
is validated instead by VoxelForge.OpStack.MazeEquivalence, which compares the
stack against GetMazeDensity over 20k points, re-checks purity across worker
threads, and brute-forces every box verdict the stack emits.

Two contract decisions, delegated and taken:

1. Eval is now two-channel (FVoxelOpSample { Density, Sdf }). Maze forces it:
   its roughness perturbs the SDF, not the density, and on density the same
   noise scales with the local gradient and is a visibly different effect. It is
   also what lets two different sources SmoothMin together later, which is the
   difference between a composed idea belonging somewhere and being punched into
   it.

2. The stack's density channel is INTERNAL convention (positive = solid),
   negated once by the caller. This REVERSES what the header said yesterday.
   Every archetype body is already written that way, so each port becomes a
   literal transcription instead of a sign-flip of every line -- on the plugin's
   documented #1 source of confusion. The SDF channel keeps standard SDF
   convention, so min() means opposite things on the two channels; the header
   says so loudly.

Also extracts spine/seal/passage from VoxelGenerator.cpp into
Public/VoxelDensityPrimitives.h so the generator and the ops share ONE copy of
three world invariants. Forwarders keep the local names, so not one of the ~20
call sites changes; bodies are byte-identical.

One thing found while writing the seal's ClassifyBox and NOT silently fixed: at
the inner edge of a seal band, 1 - Dist/Thickness can round to exactly 0.0f, so
SealFactor*BaseDensity is 0, internal density lands on 0, and the mesher counts
that as AIR. Claiming AllSolid there would be a hole. The new op keeps a 1-voxel
safety margin before it forces. Today's ClassifyTile has no such margin -- the
window is hairline and needs the archetype to produce air at exactly that z, but
it is real. Reported rather than patched, since Phase 1 does not touch that path.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:32:09 +02:00
Fr0zka 3e4ee198fe docs: progress marker before the Phase 1 Maze port
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:23:19 +02:00
Fr0zka beb66e06d4 test: unit-test the op-stack fold, and make the build actually see VoxelDensityOp.h
VoxelDensityOp.h was included by no .cpp, so the compiler would never have
looked at it -- a header committed as "ready to build" that the build ignores.
The ClassifyTile test now includes it, which is also the right home for the
fold's own test: the fold claims to reproduce the hand-written ClassifyTile, and
that claim is pure logic with no world, noise or threads behind it.

VoxelForge.OpStack.BoxVerdictFold walks the correspondence case by case,
including the one that justifies ClassifyBox existing at all: a box entirely
inside the top seal band, where the source says AllAir and the seal forces
AllSolid. A pure FillOnly would resolve that to Mixed and silently lose a tile
T1.d skips today.

Also casts UE_ARRAY_COUNT to int32 in the fixture -- signed/unsigned comparison
in a loop condition is a warning, and UE builds warnings as errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:09:17 +02:00
Fr0zka f8194dbf92 docs: correct an inaccurate claim in an earlier progress entry
The "starting Phase 0.5" entry said the ClassifyTile test self-skips when the
fixture fails to build. It does not -- all four tests fail loudly with a message
identifying it as a fixture failure. Corrected by appending, since the log is
append-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:07:49 +02:00
Fr0zka 6267af86a7 docs: tick OPSTACK-PLAN phases + final progress entry for the unattended session
Phase 0.5 and the Phase 1 skeleton marked WRITTEN / NOT COMPILED (not "done" --
the gates are not met until the tests actually run). Section 8 independent fixes
2, 5 and 6 ticked. Section 9 "Resume here" now says BUILD.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:07:32 +02:00
Fr0zka 831ee2fbf7 docs: Q3 tick the stale PENDING BUILD markers + CODEMAP rows for the new symbols
Q3: fable-idea F20 phases 1/2 and F18, REVIEW_FINDINGS perf pass 2 and batch 3,
and ARCHITECTURE's biome full-param redesign were all still carrying
"CODE-COMPLETE, PENDING BUILD" markers dated 07-04/-06/-08. Jahni confirmed on
2026-07-26 that everything is built and working (AUDIT-2026-07.md §0), so the
documents were misreporting project state. Ticked with the date they were
ticked, not just the date they were built.

Deliberately NOT ticked: ARCHITECTURE's F6 master material graph. Its C++ half
is built, but the material graph itself is editor-side work that is genuinely
still open, and ticking it would recreate the problem this queue item fixes.

CODEMAP discipline for this batch: new §3.2b (the VoxelDensityOp contract), new
§3.12 (Private/Tests), the EVoxelTileClass move into §3.2, and
FChunkBiomeCache::Invalidate under the biome types row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:06:32 +02:00
Fr0zka c188ee8262 docs: OPSTACK-DECOMPOSITION.md — the per-archetype op breakdown (Q1 + Q2)
All 8 archetypes read line by line and broken into field source / combiners /
detail modifiers / structural post, with every FStrateGenerationParams field
traced to the op that will own it.

Three findings that change the sequencing:

- The op contract needs an SDF channel alongside density. Rooms, pits and
  chimneys are SmoothMin'd in SDF space before a single carve, and three of the
  four SDF archetypes add roughness to the SDF rather than to density. A
  single-channel Eval can only overwrite, which is also why cross-source
  SmoothUnion -- 'a maze inside a mountain that looks like it belongs' -- is not
  expressible without it. Recommended before the Maze port; not applied, it is
  Jahni's call.

- Worm tunnels are why TunnelNetwork can never skip a tile. A fielded 3D-noise
  carve with no bounds forces CarveOnly everywhere, killing AllSolid for the
  most-used archetype. But its amplitude is trivially bounded by WormStrength,
  so a scalar cap recovers deep-rock skipping -- suggests one numeric bound
  belongs in Phase 2, not Phase 3 as the plan has it.

- Disturbances already carry lattice bounds that ClassifyTile discards (it only
  tests ChasmDensity > 0 strate-wide). Making them ops with real Identity tests
  is a tile-skipping win for SurfaceWorld available independently of everything
  else.

Q2 param audit: every field claimed except WaterLevelRelative, which is a
render/water property misfiled in the density struct and Lerp'd across strate
boundaries. Also flags three op names that mean different things in the cave and
surface structs and will collide the moment ops become assets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:04:00 +02:00
Fr0zka a820f140e1 docs: progress log — batch A+B landed, build gate reached
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:58:47 +02:00
Fr0zka b4d13e09ad test: regression test for AUDIT C2 (live-edit cache invalidation)
VoxelForge.Determinism.LiveEditInvalidation: sample a SurfaceWorld column,
triple the heightfield params, Initialize() again, and require the density to
have MOVED at the same point on the same thread.

The edit is chosen so it does NOT move the strate — StrateBottomWorldZ, and
therefore StrateKey, the seed and every chunk coord stay identical. The layout
version is the only thing that changes, so the test fails on the pre-fix code
and can only pass because the version is now part of the key.

Then re-checks purity on the edited world: a half-warm cache after invalidation
would show up as order dependence.

UNVERIFIED: not compiled, not run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:58:20 +02:00
Fr0zka 73f6b26f4d fix: AUDIT C2 — per-chunk caches now key on the strate layout version
Five caches under the density path were keyed on ChunkCoord (or an XY box)
alone. After RebuildStrates or an editor live edit, StrateManager rebuilds the
layout and bumps PassagesVersion, but a pooled worker whose cache is still warm
for the chunk it is asked to regenerate skips the refetch and generates with the
OLD params. RegenerateAllChunks reloads the same tile coords, often on the same
workers, so this is likely rather than exotic. Symptom: "I tweaked the strate
asset, regenerated, and one patch kept the old shape."

Fixed:
  - CP_* in GetDensityAt          (the archetype params + biome context)
  - OC_* in GetSurfaceHeightAt    (the height oracle)
  - BM_* in GetBiomeMaterialAt    (per-vertex palette)
  - TC_BiomeCache in ClassifyTile (survives across calls)
  - GSurfColCache boxes           (see below)

Two things beyond what the audit listed:

1. GSurfColCache. Its key is (XY box, StrateKey, Seed) where StrateKey is
   round(StrateBottomWorldZ). A live edit that changes terrain params WITHOUT
   moving the strate — noise frequency, mountain strength, a biome — leaves that
   key identical and serves stale columns down the whole vertical stack. This is
   the most visible form of the bug, so LayoutVersion joins the box key.

2. FChunkBiomeCache validity is a world-XY box, which says nothing about the
   FBiomeContext its cells were classified against. Refetching the context
   without invalidating the grid would leave the fix half-done, so the four
   caches call the new FChunkBiomeCache::Invalidate() on a version change.

No behavioural change at a static layout: the version only moves on Initialize.

UNVERIFIED: not compiled, not run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:58:20 +02:00
Fr0zka d41d34ecd3 feat: Phase 1 skeleton — the density operator stack contract (header only)
Public/VoxelDensityOp.h: IVoxelDensityOp (PrepareChunk / Eval / EffectOverBox /
ClassifyBox / IsXYPure), EVoxelOpEffect, EVoxelOpRole (the four roles),
EVoxelOpCombine, FVoxelOpContext, and the box-verdict fold.

Nothing is wired in: GetDensityAt is untouched, the archetype switch is intact,
no operator exists yet. This is the contract plus the reasoning behind it.

Two things worth flagging beyond OPSTACK-PLAN §3:

- ClassifyBox is NOT source-only. ApplyBoundarySeal does Max(D, SealFactor*Base)
  inside its band, i.e. it FORCES solid regardless of input. Pure direction
  (FillOnly) cannot express that: over a box that sits entirely in the top seal
  band above the terrain the source says AllAir, FillOnly then kills AllAir, both
  hypotheses die and the tile becomes Mixed — whereas ClassifyTile returns
  AllSolid there today. Not a hole, but a silent loss of exactly the trivial
  tiles T1.d exists to skip. So forcing ops override the fold, and ops after them
  still apply (a passage crossing that box takes the verdict back, as today).

- The fold reproduces the current hand-written ClassifyTile line for line; the
  mapping is written out in the header. That correspondence is the evidence the
  abstraction fits this codebase rather than being imposed on it.

Also moves EVoxelTileClass from VoxelGenerator.h to VoxelTypes.h (CODEMAP 3.2:
foundational, no UClass, everyone includes it) so the op header needs no UCLASS
dependency. All existing users reach it through VoxelTypes.h transitively.

Types are plain C++ on purpose — no UHT, no .generated.h. They become
UENUM/USTRUCT in Phase 3 when ops turn into data assets.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:55:05 +02:00
Fr0zka 6eec796403 test: Phase 0.5 — the three automation tests (density purity, ClassifyTile, DiffLayer)
The plugin had zero tests, and the docs make dozens of "bit-identical" and
"conservative verdict" claims that nothing machine-checks. OPSTACK-PLAN §4
Phase 0.5 asks for these before any op-stack work is built on top.

- VoxelForgeTestFixture.h — headless world (transient strate definitions ->
  UVoxelSettings -> a real UVoxelStrateManager::Initialize), so the tests hit
  UVoxelGenerator::GetDensityAt where the ~30 thread_local caches actually live.
  One strate per archetype, pinned via FixedStrates so slot index -> archetype
  is stable across seeds.

- DensityPurity — 10k points re-sampled in shuffled order on the same thread and
  on N worker threads, asserting BIT equality. AVoxelWorld::ValidateDeterminism
  is game-thread only and structurally cannot see worker-cache divergence, which
  is how AUDIT C2 stayed hidden. Includes a flat-field canary so a collapsed
  noise field (AUDIT C1) can't make the test pass vacuously, and a diff-layer
  pass that exercises the direct-mapped DiffSlots cache.

- ClassifyTileSoundness — scans tiles for a non-Mixed verdict, then brute-forces
  the exact mesher lattice (g in [-1, Cells+1], margin included) and asserts every
  sample is on the claimed side of IsoLevel 0. A false AllSolid/AllAir is an
  invisible collisionless hole; T1.d v1 was reverted for exactly that in June.
  Errors out rather than passing if no tile yielded a verdict to check.

- DiffLayerContention — N reader threads running the worker call mix while the
  game thread writes and Clear()s, asserting survival and a monotonic ModsVersion.

UNVERIFIED: never compiled — this module has never pulled in AutomationTest.h
and Private/Tests/ is new. See OPSTACK-PROGRESS.md for the likely error spots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:50:26 +02:00
Fr0zka 3128852d4e chore: track the design docs in git (.gitignore !*.md)
AUDIT P1: every markdown design doc except CODEMAP.md was untracked, so
ARCHITECTURE / AUDIT / OPSTACK-PLAN / fable-idea / REVIEW_FINDINGS lived
only on disk. Replaces the single !CODEMAP.md exception with !*.md.

Also makes OPSTACK-PROGRESS.md commits actually record something, which
the unattended crash-safety discipline depends on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:39:21 +02:00
Fr0zka 69fa73e07e tmp 2026-07-26 02:11:11 +02:00
Fr0zka cb61c8b2e4 potential perf regression 2026-07-04 11:30:43 +02:00
Fr0zka 6875614002 Fix Decoration Placement 2026-06-26 19:15:04 +02:00
Fr0zka e6cd852129 Another pass 2026-06-23 08:30:13 +02:00
Fr0zka db558d9e14 j 2026-06-16 03:39:22 +02:00
Fr0zka f030eec08a Upload of all files, starting point 2026-06-09 20:21:29 +02:00
Fr0zka 93489b6c1a first commit 2026-06-09 20:20:39 +02:00