From 20f6c98ec58478306ae03ca3f427016ac6e532a5 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sun, 25 Oct 2026 01:49:11 +0200 Subject: [PATCH] feat(core): profiled heightfields, slice 1: periodicHills, mirroredRelief, flatField (L53); research R22 WorldAssemblySurfaceProfileV1 on the native SurfaceWorld recipe and plan; version 0 'natural' is omitted from serialized identity, so the owner world is unchanged. periodicHills (seeded cosine hills, plan-time grade <= 15 deg), mirroredRelief (cap mirrors the floor by lambda, clearance checked) and flatField share one profile function for points, lattice classification and exact cosine-extrema intervals (0 interval misses in brute-force audits). Style-override fixtures with planned passages. Cold whole view about 1.33 s for each profile vs 1.74 s natural SW (Clang). Owner hashes identical to L52; fast/medium/full PASS. Research R22: SurfaceWorld critical path to about 1 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- .../include/VoxelForgeCore/VoxelForgeCore.h | 55 ++ Standalone/Core/src/ResolvedLayoutInternal.h | 31 +- Standalone/Core/src/WorldAssemblerInternal.h | 569 +++++++++++++++++- .../Linux53/BuildProfileFixturesL53.py | 55 ++ .../Linux53/CaptureHeightfieldProfilesL53.py | 182 ++++++ .../WorldAssemblerM3/Linux53/REPORT.md | 53 ++ .../Linux53/captures/flatField-eye.jpg | Bin 0 -> 20453 bytes .../Linux53/captures/flatField-overview.jpg | Bin 0 -> 54169 bytes .../Linux53/captures/mirroredRelief-eye.jpg | Bin 0 -> 21030 bytes .../captures/mirroredRelief-overview.jpg | Bin 0 -> 55970 bytes .../Linux53/captures/periodicHills-eye.jpg | Bin 0 -> 21015 bytes .../captures/periodicHills-overview.jpg | Bin 0 -> 55306 bytes .../WorldAssemblerM3/Research22/REPORT.md | 49 ++ Standalone/Source/StreamBenchmarkHeadless.cpp | 169 +++++- Standalone/Source/StreamBenchmarkPipeline.h | 46 +- Standalone/Source/WorldAssemblyPrototype.h | 131 +++- Standalone/Tools/verify/verify.py | 303 ++++++++++ 17 files changed, 1620 insertions(+), 23 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux53/BuildProfileFixturesL53.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux53/CaptureHeightfieldProfilesL53.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux53/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux53/captures/flatField-eye.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux53/captures/flatField-overview.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux53/captures/mirroredRelief-eye.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux53/captures/mirroredRelief-overview.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux53/captures/periodicHills-eye.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux53/captures/periodicHills-overview.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Research22/REPORT.md diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 43e4b99..2fadf34 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -425,6 +425,59 @@ namespace VoxelForgeCore std::int64_t capRidgeWavelengthSubvoxels = 256 * 64; }; + enum class WorldAssemblySurfaceProfileKindV1 : std::uint32_t + { + natural = 0, + periodicHills = 1, + mirroredRelief = 2, + flatField = 3 + }; + + // Version zero is the historical noise terrain and is intentionally omitted from + // canonical JSON and hashes. Version one uses metre-derived fixed-scale dimensions; + // phases are Q0.32 turns generated from the recipe seed during plan construction. + struct WorldAssemblySurfaceProfileV1 + { + std::uint32_t version = 0; + WorldAssemblySurfaceProfileKindV1 kind = WorldAssemblySurfaceProfileKindV1::natural; + std::int64_t h0Subvoxels = 0; + std::int64_t amplitudeXSubvoxels = 0; + std::int64_t amplitudeYSubvoxels = 0; + std::int64_t wavelengthXSubvoxels = 0; + std::int64_t wavelengthYSubvoxels = 0; + std::uint32_t phaseXTurnsQ32 = 0; + std::uint32_t phaseYTurnsQ32 = 0; + std::int64_t mirrorLambdaQ16 = 0; + std::int64_t minimumArenaClearanceSubvoxels = 0; + + friend bool operator==(const WorldAssemblySurfaceProfileV1&, + const WorldAssemblySurfaceProfileV1&) = default; + }; + + inline std::uint32_t WorldAssemblySurfaceProfileSeededPhaseQ32( + std::uint32_t seed, std::uint32_t axis) noexcept + { + std::uint64_t value = static_cast(seed) + ^ (axis == 0 ? 0x706572696f645831ull : 0x706572696f645931ull); + value += 0x9E3779B97F4A7C15ull; + value = (value ^ (value >> 30)) * 0xBF58476D1CE4E5B9ull; + value = (value ^ (value >> 27)) * 0x94D049BB133111EBull; + value ^= value >> 31; + return static_cast(value >> 32); + } + + inline void ResolveWorldAssemblySurfaceProfilePhasesV1( + WorldAssemblySurfaceProfileV1& profile, std::uint32_t seed) noexcept + { + if (profile.version == 1 + && (profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills + || profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)) + { + profile.phaseXTurnsQ32 = WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 0); + profile.phaseYTurnsQ32 = WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 1); + } + } + struct WorldAssemblyRecipeV1 { std::uint32_t schemaVersion = WorldAssemblySchemaVersion; @@ -442,6 +495,7 @@ namespace VoxelForgeCore std::int64_t biomeClimateReliefWavelengthSubvoxels = 256 * 3333; std::int64_t biomeClimateMoistureWavelengthSubvoxels = 256 * 2500; std::int64_t biomeClimateReliefContrastQ16 = 2ll << 16; + WorldAssemblySurfaceProfileV1 surfaceProfile; std::vector biomes; std::uint64_t contentHash = 0; }; @@ -501,6 +555,7 @@ namespace VoxelForgeCore std::int64_t capRidgeWavelengthSubvoxels = 256 * 64; std::int64_t waterSurfaceSubvoxels = 0; bool waterEnabled = false; + WorldAssemblySurfaceProfileV1 surfaceProfile; std::vector features; WorldAssemblyRouteConstraintsV1 routeConstraints; std::vector route; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 83e4283..38e31de 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -1993,6 +1993,10 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t &&anchor.values.size()>=3;}); const bool hasLowerTangent=hasLowerRoute&&!hasPassageArrival &&routeTangent(lower,false,lowerTangent); + const bool profiledSurfaceJoin=hasLowerTangent + &&std::any_of(lower.anchors.begin(),lower.anchors.end(), + [](const auto& anchor){return anchor.kind=="heightfield_profile" + &&anchor.values.size()>=2&&anchor.values[0]>0;}); const std::int64_t outerRingRadius=std::max(1, placementRadius-mouthRadius-qPerMetre/2); @@ -2237,7 +2241,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t helixEndIndex=xy.size()-1; hasAnchorHelix=helixEndIndex>0; } - const std::int64_t routeGradeQ16=hasAnchorHelix + const std::int64_t routeGradeQ16=profiledSurfaceJoin?8000:hasAnchorHelix &&lower.generatorKind=="SurfaceWorld"?8000 :style==0&&anchoredIslandPassage&&!anchoredSpiral?15500:TargetGradeQ16; const std::array firstRing=ringPoint(phase); @@ -2310,6 +2314,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t std::uint32_t lowerPhase=static_cast(PassageSeed(seed, upper.strateIndex,lower.strateIndex,connection,0x4c505248u)%16); std::int64_t bestAlignment=std::numeric_limits::min(); + bool lowerRouteReversed=false; const std::uint32_t hashedLowerPhase=lowerPhase; for(std::uint32_t offset=0;offset<16;++offset) { @@ -2326,8 +2331,32 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t if(score>bestAlignment) {bestAlignment=score;lowerPhase=candidate;} } + // A route is walkable in either direction. If the approach ring lies on + // the wrong side of its first route tangent, attach to the same endpoint + // while traversing the route backwards instead of rejecting the layout. + if(bestAlignment==std::numeric_limits::min()) + { + for(std::uint32_t offset=0;offset<16;++offset) + { + const std::uint32_t candidate=static_cast( + (hashedLowerPhase+offset)%16); + const auto terminal=ringPoint(candidate,loops); + const std::int64_t dx=lowerMouth[0]-terminal[0]; + const std::int64_t dy=lowerMouth[1]-terminal[1]; + const std::int64_t alignment=dx*lowerTangent[0]+dy*lowerTangent[1]; + const std::int64_t cross=std::abs(dx*lowerTangent[1]-dy*lowerTangent[0]); + if(alignment>=0||cross==0)continue; + const auto length=distanceQ(dx,dy); + const std::int64_t score=-alignment*65536/static_cast(length); + if(score>bestAlignment) + {bestAlignment=score;lowerPhase=candidate;lowerRouteReversed=true;} + } + } if(bestAlignment==std::numeric_limits::min()) {error=passage.stableId+" could not find a non-overlapping lower landing approach";return false;} + std::printf("[ResolvedPassageLowerApproach] id=%s direction=%s alignment_q16=%lld status=PASS\n", + passage.stableId.c_str(),lowerRouteReversed?"reverse":"forward", + static_cast(bestAlignment)); if(phase!=lowerPhase) for(int step=1;step<=16;++step) { diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h index c6a446b..ac11697 100644 --- a/Standalone/Core/src/WorldAssemblerInternal.h +++ b/Standalone/Core/src/WorldAssemblerInternal.h @@ -145,6 +145,12 @@ namespace world_assembly_detail }; constexpr std::int64_t Q16 = 1ll << 16; + constexpr std::int64_t Q32 = 1ll << 32; + constexpr std::int64_t SurfaceProfileSubvoxelsPerMetre = + WorldAssemblySubvoxelsPerVoxel * 4; + constexpr std::int64_t SurfaceProfileMinimumClearanceSubvoxels = 2253; + constexpr std::int64_t SurfaceProfileFightClearanceSubvoxels = + 8 * SurfaceProfileSubvoxelsPerMetre; constexpr std::int64_t MinimumCliffShelfThicknessSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel; constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll; constexpr std::size_t MaxBiomeCount = 8; @@ -228,6 +234,114 @@ namespace world_assembly_detail inline bool OutsideRange(std::int64_t value) { return value < -MaxSerializedMagnitude || value > MaxSerializedMagnitude; } + inline bool IsNaturalSurfaceProfile(const WorldAssemblySurfaceProfileV1& profile) + { return profile.version == 0 && profile == WorldAssemblySurfaceProfileV1{}; } + + inline bool ValidateSurfaceProfile(const WorldAssemblySurfaceProfileV1& profile, + std::uint32_t seed, std::string& error) + { + if (profile.version == 0) + { + if (!IsNaturalSurfaceProfile(profile)) + { error = "natural SurfaceWorld profile must use the all-zero default payload"; return false; } + return true; + } + if (profile.version != 1 + || !ValidScalar(profile.h0Subvoxels) + || !ValidScalar(profile.amplitudeXSubvoxels) + || !ValidScalar(profile.amplitudeYSubvoxels) + || !ValidScalar(profile.wavelengthXSubvoxels) + || !ValidScalar(profile.wavelengthYSubvoxels) + || !ValidScalar(profile.mirrorLambdaQ16) + || !ValidScalar(profile.minimumArenaClearanceSubvoxels) + || profile.minimumArenaClearanceSubvoxels < 0 + || profile.minimumArenaClearanceSubvoxels > 32 * SurfaceProfileSubvoxelsPerMetre) + { error = "SurfaceWorld profile version or fixed-scale value is outside its supported range"; return false; } + + constexpr std::int64_t MinimumPeriodAmplitude = SurfaceProfileSubvoxelsPerMetre / 2; + constexpr std::int64_t MaximumPeriodicAmplitude = 3 * SurfaceProfileSubvoxelsPerMetre; + constexpr std::int64_t MaximumMirrorAmplitude = 2 * SurfaceProfileSubvoxelsPerMetre; + constexpr std::int64_t MinimumWavelength = 80 * SurfaceProfileSubvoxelsPerMetre; + constexpr std::int64_t MaximumWavelength = 160 * SurfaceProfileSubvoxelsPerMetre; + const bool hasArenaClearance = profile.minimumArenaClearanceSubvoxels == 0 + || profile.minimumArenaClearanceSubvoxels >= SurfaceProfileFightClearanceSubvoxels; + if (!hasArenaClearance) + { error = "SurfaceWorld arena clearance must be omitted or at least eight metres"; return false; } + + if (profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills + || profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief) + { + const std::int64_t maximumAmplitude = + profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills + ? MaximumPeriodicAmplitude : MaximumMirrorAmplitude; + if (profile.amplitudeXSubvoxels < MinimumPeriodAmplitude + || profile.amplitudeYSubvoxels < MinimumPeriodAmplitude + || profile.amplitudeXSubvoxels > maximumAmplitude + || profile.amplitudeYSubvoxels > maximumAmplitude + || profile.wavelengthXSubvoxels < MinimumWavelength + || profile.wavelengthYSubvoxels < MinimumWavelength + || profile.wavelengthXSubvoxels > MaximumWavelength + || profile.wavelengthYSubvoxels > MaximumWavelength) + { error = "periodic SurfaceWorld amplitudes or wavelengths are outside the R20 metre ranges"; return false; } + + // Round every gradient component upward before applying the 15-degree bound. + // This makes the integer plan-time check conservative around division boundaries. + constexpr std::int64_t TwoPiQ16Ceiling = 411775; + constexpr std::int64_t Tan15Q16Floor = 17560; + const auto component = [](std::int64_t amplitude, std::int64_t wavelength) + { + const std::int64_t numerator = TwoPiQ16Ceiling * amplitude; + return (numerator + wavelength - 1) / wavelength; + }; + const std::int64_t gx = component(profile.amplitudeXSubvoxels, + profile.wavelengthXSubvoxels); + const std::int64_t gy = component(profile.amplitudeYSubvoxels, + profile.wavelengthYSubvoxels); + if (gx * gx + gy * gy > Tan15Q16Floor * Tan15Q16Floor) + { error = "periodic SurfaceWorld maximum walk grade exceeds tan(15 degrees)"; return false; } + + if (profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills) + { + if (profile.mirrorLambdaQ16 != 0 + || profile.phaseXTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 0) + || profile.phaseYTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 1)) + { error = "periodic-hill phases must be the stable seed-derived Q0.32 phases"; return false; } + } + else if (profile.mirrorLambdaQ16 < 49152 || profile.mirrorLambdaQ16 > Q16 + || profile.phaseXTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 0) + || profile.phaseYTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 1)) + { error = "mirrored-relief lambda or seeded phases are outside their supported range"; return false; } + } + else if (profile.kind == WorldAssemblySurfaceProfileKindV1::flatField) + { + if (profile.amplitudeXSubvoxels != 0 || profile.amplitudeYSubvoxels != 0 + || profile.wavelengthXSubvoxels != 0 || profile.wavelengthYSubvoxels != 0 + || profile.phaseXTurnsQ32 != 0 || profile.phaseYTurnsQ32 != 0 + || profile.mirrorLambdaQ16 != 0) + { error = "flatField must not carry periodic or mirrored relief parameters"; return false; } + } + else + { error = "SurfaceWorld profile kind is unsupported"; return false; } + return true; + } + + inline void HashSurfaceProfile(std::uint64_t& hash, + const WorldAssemblySurfaceProfileV1& profile) + { + HashWord(hash, 0x5355524650524f31ull, 8); + HashWord(hash, profile.version, 4); + HashWord(hash, static_cast(profile.kind), 4); + HashWord(hash, static_cast(profile.h0Subvoxels), 8); + HashWord(hash, static_cast(profile.amplitudeXSubvoxels), 8); + HashWord(hash, static_cast(profile.amplitudeYSubvoxels), 8); + HashWord(hash, static_cast(profile.wavelengthXSubvoxels), 8); + HashWord(hash, static_cast(profile.wavelengthYSubvoxels), 8); + HashWord(hash, profile.phaseXTurnsQ32, 4); + HashWord(hash, profile.phaseYTurnsQ32, 4); + HashWord(hash, static_cast(profile.mirrorLambdaQ16), 8); + HashWord(hash, static_cast(profile.minimumArenaClearanceSubvoxels), 8); + } + inline bool ValidateRecipe(const WorldAssemblyRecipeV1& recipe, std::string& error) { if (recipe.schemaVersion != WorldAssemblySchemaVersion @@ -296,16 +410,57 @@ namespace world_assembly_detail || recipe.biomeClimateMoistureWavelengthSubvoxels <= 0 || recipe.biomeClimateReliefContrastQ16 < 0)) { error = "world assembly climate parameters are outside the supported fixed-scale range"; return false; } + if (!ValidateSurfaceProfile(recipe.surfaceProfile, recipe.seed, error)) return false; + return true; + } + + inline bool ValidateSurfaceProfileGeometry(const WorldAssemblySurfaceProfileV1& profile, + const WorldAssemblyStratumPlanV1& stratum, std::int64_t layoutChunkWidthSubvoxels, + std::string& error) + { + if (profile.version == 0) return true; + const std::int64_t relief = profile.amplitudeXSubvoxels + profile.amplitudeYSubvoxels; + const std::int64_t groundMinimum = profile.h0Subvoxels - relief - 2; + const std::int64_t groundMaximum = profile.h0Subvoxels + relief + 2; + std::int64_t capSwing = 0; + if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief) + { + const std::int64_t product = relief * profile.mirrorLambdaQ16; + capSwing = (product + Q16 - 1) / Q16 + 2; + } + const std::int64_t capMinimum = stratum.capHeightSubvoxels - capSwing; + const std::int64_t capMaximum = stratum.capHeightSubvoxels + capSwing; + const std::int64_t clearance = capMinimum - groundMaximum; + const std::int64_t requiredClearance = std::max( + SurfaceProfileMinimumClearanceSubvoxels, + profile.minimumArenaClearanceSubvoxels); + if (clearance < requiredClearance) + { error = "SurfaceWorld profile does not preserve its required floor-to-cap clearance"; return false; } + + const std::int64_t stratumMinimum = static_cast(stratum.bottomChunkZ) + * layoutChunkWidthSubvoxels; + const std::int64_t stratumMaximum = (static_cast(stratum.topChunkZ) + 1) + * layoutChunkWidthSubvoxels; + if (groundMinimum <= stratumMinimum || capMaximum >= stratumMaximum) + { error = "SurfaceWorld profile escapes its finite stratum Z bounds"; return false; } return true; } inline bool ValidateStratum(const WorldAssemblyStratumPlanV1& stratum, - std::int64_t layoutChunkWidthSubvoxels, std::string& error) + const WorldAssemblyRecipeV1& recipe, std::string& error) { + const std::int64_t layoutChunkWidthSubvoxels = recipe.layoutChunkWidthSubvoxels; if (stratum.strateIndex < 0 || stratum.topChunkZ < stratum.bottomChunkZ || stratum.capWavelengthSubvoxels <= 0 || stratum.capUndulationSubvoxels < 0 || stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount) { error = "world assembly stratum plan has an invalid extent or exceeds its bounded payload"; return false; } + const WorldAssemblyRecipeV1& effectiveRecipe = stratum.hasRecipeOverride + ? stratum.recipeOverride : recipe; + if (!ValidateSurfaceProfile(stratum.surfaceProfile, effectiveRecipe.seed, error)) return false; + if (stratum.surfaceProfile != effectiveRecipe.surfaceProfile) + { error = "SurfaceWorld recipe and stratum plan profile payloads do not match"; return false; } + if (!ValidateSurfaceProfileGeometry(stratum.surfaceProfile, stratum, + layoutChunkWidthSubvoxels, error)) return false; if (!ValidScalar(stratum.capHeightSubvoxels) || !ValidScalar(stratum.capUndulationSubvoxels) || !ValidScalar(stratum.capWavelengthSubvoxels) || !ValidScalar(stratum.capDetailAmplitudeSubvoxels) @@ -466,6 +621,8 @@ namespace world_assembly_detail HashWord(hash, static_cast(value), 8); } } + if (recipe.surfaceProfile.version != 0) + HashSurfaceProfile(hash, recipe.surfaceProfile); return hash; } @@ -525,6 +682,8 @@ namespace world_assembly_detail for (const auto value : point.positionSubvoxels) HashWord(hash, static_cast(value), 8); HashWord(hash, static_cast(point.radiusSubvoxels), 8); } + if (stratum.surfaceProfile.version != 0) + HashSurfaceProfile(hash, stratum.surfaceProfile); return hash; } @@ -627,6 +786,59 @@ namespace world_assembly_detail return out.str(); } + inline const char* SurfaceProfileKindName(WorldAssemblySurfaceProfileKindV1 kind) + { + switch (kind) + { + case WorldAssemblySurfaceProfileKindV1::natural: return "natural"; + case WorldAssemblySurfaceProfileKindV1::periodicHills: return "periodicHills"; + case WorldAssemblySurfaceProfileKindV1::mirroredRelief: return "mirroredRelief"; + case WorldAssemblySurfaceProfileKindV1::flatField: return "flatField"; + } + return "unsupported"; + } + + inline bool ParseSurfaceProfile(const VFJson::Value& value, + WorldAssemblySurfaceProfileV1& profile) + { + if (!value.IsObject() || !ReadU32(value.Get("version"), profile.version)) return false; + const auto* kind = std::get_if(&value.Get("kind").Data); + if (kind == nullptr) return false; + if (*kind == "periodicHills") profile.kind = WorldAssemblySurfaceProfileKindV1::periodicHills; + else if (*kind == "mirroredRelief") profile.kind = WorldAssemblySurfaceProfileKindV1::mirroredRelief; + else if (*kind == "flatField") profile.kind = WorldAssemblySurfaceProfileKindV1::flatField; + else if (*kind == "natural") profile.kind = WorldAssemblySurfaceProfileKindV1::natural; + else return false; + return ReadInteger(value.Get("h0_q"), profile.h0Subvoxels) + && ReadInteger(value.Get("amplitude_x_q"), profile.amplitudeXSubvoxels) + && ReadInteger(value.Get("amplitude_y_q"), profile.amplitudeYSubvoxels) + && ReadInteger(value.Get("wavelength_x_q"), profile.wavelengthXSubvoxels) + && ReadInteger(value.Get("wavelength_y_q"), profile.wavelengthYSubvoxels) + && ReadU32(value.Get("phase_x_turns_q32"), profile.phaseXTurnsQ32) + && ReadU32(value.Get("phase_y_turns_q32"), profile.phaseYTurnsQ32) + && ReadInteger(value.Get("mirror_lambda_q16"), profile.mirrorLambdaQ16) + && ReadInteger(value.Get("minimum_arena_clearance_q"), + profile.minimumArenaClearanceSubvoxels); + } + + inline std::string SurfaceProfileJson(const WorldAssemblySurfaceProfileV1& profile) + { + std::ostringstream out; + out << "{\"version\":" << profile.version + << ",\"kind\":\"" << SurfaceProfileKindName(profile.kind) << "\"" + << ",\"h0_q\":" << profile.h0Subvoxels + << ",\"amplitude_x_q\":" << profile.amplitudeXSubvoxels + << ",\"amplitude_y_q\":" << profile.amplitudeYSubvoxels + << ",\"wavelength_x_q\":" << profile.wavelengthXSubvoxels + << ",\"wavelength_y_q\":" << profile.wavelengthYSubvoxels + << ",\"phase_x_turns_q32\":" << profile.phaseXTurnsQ32 + << ",\"phase_y_turns_q32\":" << profile.phaseYTurnsQ32 + << ",\"mirror_lambda_q16\":" << profile.mirrorLambdaQ16 + << ",\"minimum_arena_clearance_q\":" + << profile.minimumArenaClearanceSubvoxels << '}'; + return out.str(); + } + inline bool ParseRecipe(const VFJson::Value& value, WorldAssemblyRecipeV1& recipe, std::string& error) { @@ -647,6 +859,10 @@ namespace world_assembly_detail || !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels) || !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16))) { error = "world assembly recipe is missing evaluator-2 climate parameters"; return false; } + const VFJson::Value& surfaceProfileValue = value.Get("surface_profile"); + if (!std::holds_alternative(surfaceProfileValue.Data) + && !ParseSurfaceProfile(surfaceProfileValue, recipe.surfaceProfile)) + { error = "world assembly recipe has a malformed versioned SurfaceWorld profile"; return false; } for (const auto& item : value.Get("biomes").AsArray()) { WorldAssemblyBiomeProfileV1 biome; @@ -709,6 +925,8 @@ namespace world_assembly_detail out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels << ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels << ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16; + if (recipe.surfaceProfile.version != 0) + out << ",\"surface_profile\":" << SurfaceProfileJson(recipe.surfaceProfile); out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":["; for (std::size_t i = 0; i < recipe.biomes.size(); ++i) { @@ -797,6 +1015,10 @@ namespace world_assembly_detail { error = "world assembly stratum has malformed optional evaluator-2 fields"; return false; } if(hasWaterBoolean)stratum.waterEnabled=waterEnabled.Boolean(false); if(hasWaterNumber)stratum.waterEnabled=waterEnabled.Number()!=0.0; + const VFJson::Value& surfaceProfileValue = item.Get("surface_profile"); + if (!std::holds_alternative(surfaceProfileValue.Data) + && !ParseSurfaceProfile(surfaceProfileValue, stratum.surfaceProfile)) + { error = "world assembly stratum has a malformed versioned SurfaceWorld profile"; return false; } const VFJson::Value& routeConstraintsValue = item.Get("route_constraints"); if (!std::holds_alternative(routeConstraintsValue.Data)) { @@ -1367,6 +1589,196 @@ namespace world_assembly_detail return SmoothQ16(static_cast(std::clamp(edge*Q16/feather,0,Q16))); } + constexpr std::int64_t Q30 = 1ll << 30; + constexpr std::array CordicAtanTurnsQ32{{ + 536870912,316933406,167458907,85004756,42667331,21354465,10679838,5340245, + 2670163,1335087,667544,333772,166886,83443,41722,20861,10430,5215,2608, + 1304,652,326,163,81,41,20,10,5,3,1}}; + + inline std::array CordicSinCosQuarterQ30(std::uint32_t angleQ32) + { + if (angleQ32 == 0) return {Q30, 0}; + std::int64_t x = 652032874, y = 0; + std::int64_t z = angleQ32; + for (std::size_t i = 0; i < CordicAtanTurnsQ32.size(); ++i) + { + const std::int64_t oldX = x; + if (z >= 0) + { + x -= y >> i; + y += oldX >> i; + z -= CordicAtanTurnsQ32[i]; + } + else + { + x += y >> i; + y -= oldX >> i; + z += CordicAtanTurnsQ32[i]; + } + } + return {std::clamp(x, -Q30, Q30), + std::clamp(y, -Q30, Q30)}; + } + + inline std::int64_t CosineQ30(std::uint32_t phaseTurnsQ32) + { + constexpr std::uint32_t Quarter = 1u << 30; + if (phaseTurnsQ32 == 0) return Q30; + if (phaseTurnsQ32 == Quarter) return 0; + if (phaseTurnsQ32 == 2u * Quarter) return -Q30; + if (phaseTurnsQ32 == 3u * Quarter) return 0; + constexpr std::uint32_t TableBits = 16; + constexpr std::uint32_t TableSize = 1u << TableBits; + constexpr std::uint32_t FractionMask = (1u << (32 - TableBits)) - 1u; + static const std::array table = [] + { + std::array values{}; + constexpr std::uint32_t QuarterTableSize = TableSize / 4; + constexpr std::uint32_t QuarterStepQ32 = 1u << (30 - (TableBits - 2)); + std::array quarter{}; + for (std::uint32_t offset = 0; offset <= QuarterTableSize; ++offset) + { + const std::int64_t cosine = CordicSinCosQuarterQ30( + offset * QuarterStepQ32)[0]; + const std::int64_t monotone = offset == 0 ? cosine + : std::min(quarter[offset - 1], cosine); + quarter[offset] = static_cast(monotone); + } + for (std::uint32_t index = 0; index < TableSize; ++index) + { + const std::uint32_t quadrant = index / QuarterTableSize; + const std::uint32_t offset = index % QuarterTableSize; + const std::uint32_t mirroredOffset = QuarterTableSize - offset; + const std::int32_t value = quadrant == 0 ? quarter[offset] + : quadrant == 1 ? -quarter[mirroredOffset] + : quadrant == 2 ? -quarter[offset] : quarter[mirroredOffset]; + values[index] = value; + } + return values; + }(); + const std::uint32_t index = phaseTurnsQ32 >> (32 - TableBits); + const std::uint32_t fraction = phaseTurnsQ32 & FractionMask; + const std::int64_t first = table[index]; + const std::int64_t second = table[(index + 1u) & (TableSize - 1u)]; + const std::int64_t numerator = (second - first) * fraction; + const std::int64_t rounded = numerator >= 0 + ? numerator + (1ll << (31 - TableBits)) + : numerator - (1ll << (31 - TableBits)); + // The fixed-point table interpolation is monotone within each quadrant; + // cosine interval bounds therefore still attain extrema at their endpoints + // or at the exact quarter-turn extrema checked below. + return first + rounded / (1ll << (32 - TableBits)); + } + + inline std::uint32_t SurfaceProfileTurnAtCoordinate(std::int64_t coordinate, + std::int64_t wavelength, std::uint32_t phaseTurnsQ32) + { + const std::uint64_t coordinateTurns = static_cast( + FloorMod(coordinate, wavelength)) * static_cast(Q32) + / static_cast(wavelength); + return phaseTurnsQ32 + static_cast(coordinateTurns); + } + + struct CosineRangeQ30 + { + std::int64_t minimum = -Q30; + std::int64_t maximum = Q30; + }; + + inline CosineRangeQ30 SurfaceProfileCosineRangeQ30(std::int64_t minimumCoordinate, + std::int64_t maximumCoordinate, std::int64_t wavelength, + std::uint32_t phaseTurnsQ32) + { + if (wavelength <= 0 || minimumCoordinate > maximumCoordinate + || maximumCoordinate - minimumCoordinate >= wavelength) + return {}; + + CosineRangeQ30 range; + const std::int64_t cosineAtMinimum = CosineQ30( + SurfaceProfileTurnAtCoordinate(minimumCoordinate, wavelength, phaseTurnsQ32)); + const std::int64_t cosineAtMaximum = CosineQ30( + SurfaceProfileTurnAtCoordinate(maximumCoordinate, wavelength, phaseTurnsQ32)); + range.minimum = std::min(cosineAtMinimum, cosineAtMaximum); + range.maximum = std::max(cosineAtMinimum, cosineAtMaximum); + + // Coordinate turns are Q0.32. Test the exact analytic extrema in a common + // rational numerator domain; one Q0.32 unit of endpoint slack accounts for + // the fixed phase conversion used by point(). + const std::uint64_t periodNumerator = static_cast(wavelength) + * static_cast(Q32); + const std::uint64_t startNumerator = ( + static_cast(FloorMod(minimumCoordinate, wavelength)) + * static_cast(Q32) + + static_cast(phaseTurnsQ32) + * static_cast(wavelength)) % periodNumerator; + const std::uint64_t spanNumerator = static_cast( + maximumCoordinate - minimumCoordinate) * static_cast(Q32); + const auto includesExtremum = [&](std::uint64_t target) + { + const std::uint64_t forward = target >= startNumerator + ? target - startNumerator : periodNumerator - startNumerator + target; + const std::uint64_t backward = periodNumerator - forward; + return forward <= spanNumerator + wavelength + || backward <= wavelength; + }; + if (includesExtremum(0)) range.maximum = Q30; + if (includesExtremum(periodNumerator / 2)) range.minimum = -Q30; + return range; + } + + inline std::int64_t MulQ30(std::int64_t value, std::int64_t cosineQ30) + { return (value * cosineQ30) / Q30; } + + struct SurfaceProfileColumnBounds + { + std::int64_t groundMinimum = 0; + std::int64_t groundMaximum = 0; + std::int64_t capMinimum = 0; + std::int64_t capMaximum = 0; + }; + + inline SurfaceProfileColumnBounds ProfileColumnBounds( + const WorldAssemblySurfaceProfileV1& profile, + const WorldAssemblyStratumPlanV1& plan, + std::int64_t minimumX, std::int64_t minimumY, + std::int64_t maximumX, std::int64_t maximumY) + { + SurfaceProfileColumnBounds bounds; + if (profile.kind == WorldAssemblySurfaceProfileKindV1::flatField) + { + bounds.groundMinimum = profile.h0Subvoxels; + bounds.groundMaximum = profile.h0Subvoxels; + bounds.capMinimum = plan.capHeightSubvoxels; + bounds.capMaximum = plan.capHeightSubvoxels; + return bounds; + } + const CosineRangeQ30 x = SurfaceProfileCosineRangeQ30(minimumX, maximumX, + profile.wavelengthXSubvoxels, profile.phaseXTurnsQ32); + const CosineRangeQ30 y = SurfaceProfileCosineRangeQ30(minimumY, maximumY, + profile.wavelengthYSubvoxels, profile.phaseYTurnsQ32); + const std::int64_t groundXMinimum = MulQ30(profile.amplitudeXSubvoxels, x.minimum); + const std::int64_t groundXMaximum = MulQ30(profile.amplitudeXSubvoxels, x.maximum); + const std::int64_t groundYMinimum = MulQ30(profile.amplitudeYSubvoxels, y.minimum); + const std::int64_t groundYMaximum = MulQ30(profile.amplitudeYSubvoxels, y.maximum); + bounds.groundMinimum = profile.h0Subvoxels + groundXMinimum + groundYMinimum - 2; + bounds.groundMaximum = profile.h0Subvoxels + groundXMaximum + groundYMaximum + 2; + if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief) + { + const std::int64_t reliefMinimum = groundXMinimum + groundYMinimum; + const std::int64_t reliefMaximum = groundXMaximum + groundYMaximum; + bounds.capMinimum = plan.capHeightSubvoxels + - MulQ16(reliefMaximum, profile.mirrorLambdaQ16) - 2; + bounds.capMaximum = plan.capHeightSubvoxels + - MulQ16(reliefMinimum, profile.mirrorLambdaQ16) + 2; + } + else + { + bounds.capMinimum = plan.capHeightSubvoxels; + bounds.capMaximum = plan.capHeightSubvoxels; + } + return bounds; + } + struct HeightfieldColumnCacheEntry { const WorldAssemblyRecipeV1* recipe = nullptr; @@ -1415,6 +1827,61 @@ namespace world_assembly_detail HeightfieldPointSample out; if(recipe.evaluatorVersion>=2) { + if (recipe.surfaceProfile.version == 1) + { + const auto& profile = recipe.surfaceProfile; + std::int64_t relief = 0; + if (profile.kind != WorldAssemblySurfaceProfileKindV1::flatField) + { + const std::int64_t cosineX = CosineQ30(SurfaceProfileTurnAtCoordinate( + x, profile.wavelengthXSubvoxels, profile.phaseXTurnsQ32)); + const std::int64_t cosineY = CosineQ30(SurfaceProfileTurnAtCoordinate( + y, profile.wavelengthYSubvoxels, profile.phaseYTurnsQ32)); + relief = MulQ30(profile.amplitudeXSubvoxels, cosineX) + + MulQ30(profile.amplitudeYSubvoxels, cosineY); + } + out.groundHeightSubvoxels = profile.h0Subvoxels + relief; + out.capHeightSubvoxels = profile.kind + == WorldAssemblySurfaceProfileKindV1::mirroredRelief + ? plan.capHeightSubvoxels - MulQ16(relief, profile.mirrorLambdaQ16) + : plan.capHeightSubvoxels; + out.biomeId = recipe.biomes.front().stableId; + out.neighborBiomeId = out.biomeId; + out.biomeWeightQ16 = static_cast(Q16); + const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find( + &recipe, &plan, x, y, 0, true); + for (const auto& feature : FeatureRange{plan.features, list}) + { + if (feature.type == 1) + { + const std::uint32_t fade = FeatureFadeQ16(feature, x, y, + feature.parametersSubvoxels[1]); + if (fade) out.groundHeightSubvoxels += MulQ16( + feature.parametersSubvoxels[0], fade); + } + else if (feature.type == 3) + { + const std::uint32_t fade = FeatureFadeQ16(feature, x, y, + feature.parametersSubvoxels[2]); + const std::int64_t wave = std::max( + 1, feature.parametersSubvoxels[1]); + if (fade) + { + const std::uint32_t featureSeed = static_cast(feature.stableId); + const std::int64_t nx = NoiseCoordinateQ16(x, wave) + + SeedOffsetQ16(featureSeed, 1700); + const std::int64_t ny = NoiseCoordinateQ16(y, wave) + + SeedOffsetQ16(featureSeed, 2900); + const std::int32_t noise = FractalNoiseQ16(nx, ny, + SeedOffsetQ16(featureSeed, 4100), 3); + out.capHeightSubvoxels += MulQ16(MulQ16(noise, + feature.parametersSubvoxels[0]), fade); + } + } + } + out.waterSurfaceSubvoxels = plan.waterSurfaceSubvoxels; + return out; + } const BiomeSelectionV2 selection=SelectBiomeV2(recipe,x,y); const auto& primary=recipe.biomes[selection.primary]; const auto& neighbor=recipe.biomes[selection.neighbor]; @@ -1742,6 +2209,16 @@ std::optional WorldAssemblyManifest::FromJson( bool WorldAssemblyManifest::finalize(std::string& error) { + ResolveWorldAssemblySurfaceProfilePhasesV1(recipe.surfaceProfile, recipe.seed); + for (auto& stratum : plan.strates) + { + WorldAssemblyRecipeV1& effectiveRecipe = stratum.hasRecipeOverride + ? stratum.recipeOverride : recipe; + ResolveWorldAssemblySurfaceProfilePhasesV1(effectiveRecipe.surfaceProfile, + effectiveRecipe.seed); + ResolveWorldAssemblySurfaceProfilePhasesV1(stratum.surfaceProfile, + effectiveRecipe.seed); + } if (schemaVersion != WorldAssemblySchemaVersion || plan.schemaVersion != WorldAssemblySchemaVersion || !world_assembly_detail::ValidateRecipe(recipe, error)) { if (error.empty()) error = "world assembly manifest or plan version is unsupported"; return false; } @@ -1759,7 +2236,7 @@ bool WorldAssemblyManifest::finalize(std::string& error) { error = "SurfaceWorld stratum recipe seed does not match the world assembly season seed"; return false; } stratum.recipeOverride.contentHash = world_assembly_detail::HashRecipe(stratum.recipeOverride); } - if (!world_assembly_detail::ValidateStratum(stratum, recipe.layoutChunkWidthSubvoxels, error)) return false; + if (!world_assembly_detail::ValidateStratum(stratum, recipe, error)) return false; if (recipe.evaluatorVersion >= 4) { if (stratum.routeConstraints.version != 1 || stratum.route.size() < 2) @@ -1882,6 +2359,9 @@ std::string WorldAssemblyManifest::ToJson() const if (stratum.hasRecipeOverride) out << ",\"recipe_override\":" << world_assembly_detail::RecipeJson(stratum.recipeOverride); + if (stratum.surfaceProfile.version != 0) + out << ",\"surface_profile\":" + << world_assembly_detail::SurfaceProfileJson(stratum.surfaceProfile); out << ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash) << "\",\"features\":["; @@ -1957,6 +2437,8 @@ bool Heightfield2D::isValid() const noexcept || recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount || recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0 || recipe_->biomeWarpSubvoxels < 0 || recipe_->biomeBorderBlendSubvoxels < 0 + || recipe_->surfaceProfile != plan_->surfaceProfile + || recipe_->surfaceProfile.version > 1 || plan_->strateIndex < 0 || plan_->topChunkZ < plan_->bottomChunkZ || plan_->capWavelengthSubvoxels <= 0 || plan_->capUndulationSubvoxels < 0) return false; @@ -1973,6 +2455,32 @@ bool Heightfield2D::isValid() const noexcept std::array Heightfield2D::capHeightBounds() const noexcept { if(!isValid())return {0,0}; + if (recipe_->surfaceProfile.version == 1) + { + const auto& profile = recipe_->surfaceProfile; + std::int64_t swing = 0; + if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief) + { + const std::int64_t relief = profile.amplitudeXSubvoxels + + profile.amplitudeYSubvoxels; + swing = (relief * profile.mirrorLambdaQ16 + world_assembly_detail::Q16 - 1) + / world_assembly_detail::Q16 + 2; + } + std::int64_t featureAmplitude = 0; + for (const auto& feature : plan_->features) + if (feature.type == 3) + featureAmplitude += std::abs(feature.parametersSubvoxels[0]); + std::int64_t minimum = plan_->capHeightSubvoxels - swing - featureAmplitude; + std::int64_t maximum = plan_->capHeightSubvoxels + swing + featureAmplitude; + if (recipe_->evaluatorVersion >= 5) + { + const std::int64_t upperBoundary = (static_cast(plan_->topChunkZ) + 1) + * recipe_->layoutChunkWidthSubvoxels; + minimum = std::min(minimum, upperBoundary); + maximum = std::min(maximum, upperBoundary); + } + return {minimum, maximum}; + } long double Minimum=std::numeric_limits::infinity(); long double Maximum=-std::numeric_limits::infinity(); const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome) @@ -2178,6 +2686,15 @@ HeightfieldBoxClass Heightfield2D::classifyLatticeBox(std::int64_t minimumX, &&feature.minimumSubvoxels[2]<=maximumZ&&feature.maximumSubvoxels[2]>=minimumZ) return HeightfieldBoxClass::mixed; } + if (recipe_->surfaceProfile.version == 1) + { + const FieldInterval proof = interval(minimumX, minimumY, minimumZ, + maximumX, maximumY, maximumZ); + if (!proof.known) return HeightfieldBoxClass::unknown; + if (proof.minimumDensitySubvoxels > 0) return HeightfieldBoxClass::allAir; + if (proof.maximumDensitySubvoxels < 0) return HeightfieldBoxClass::allSolid; + return HeightfieldBoxClass::mixed; + } const std::int64_t stride=static_cast(stepVoxels) *WorldAssemblySubvoxelsPerVoxel; if((maximumX-minimumX)%stride!=0||(maximumY-minimumY)%stride!=0 @@ -2220,7 +2737,29 @@ FieldInterval Heightfield2D::interval(std::int64_t minX, std::int64_t minY, std::int64_t groundMax = std::numeric_limits::min(); std::int64_t capMin=plan_->capHeightSubvoxels-plan_->capUndulationSubvoxels; std::int64_t capMax=plan_->capHeightSubvoxels+plan_->capUndulationSubvoxels; - if(recipe_->evaluatorVersion>=2) + if (recipe_->surfaceProfile.version == 1) + { + const auto profileBounds = world_assembly_detail::ProfileColumnBounds( + recipe_->surfaceProfile, *plan_, minX, minY, maxX, maxY); + groundMin = profileBounds.groundMinimum; + groundMax = profileBounds.groundMaximum; + capMin = profileBounds.capMinimum; + capMax = profileBounds.capMaximum; + std::int64_t groundFeatureMagnitude = 0; + std::int64_t capFeatureMagnitude = 0; + for (const auto& feature : plan_->features) + { + if (feature.type == 1) + groundFeatureMagnitude += std::abs(feature.parametersSubvoxels[0]); + if (feature.type == 3) + capFeatureMagnitude += std::abs(feature.parametersSubvoxels[0]); + } + groundMin -= groundFeatureMagnitude; + groundMax += groundFeatureMagnitude; + capMin -= capFeatureMagnitude; + capMax += capFeatureMagnitude; + } + else if(recipe_->evaluatorVersion>=2) { for(const auto& biome:recipe_->biomes) { @@ -2313,6 +2852,24 @@ FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept { FieldDetailEnvelope out; if (!isValid()) return out; + if (recipe_->surfaceProfile.version == 1) + { + const auto& profile = recipe_->surfaceProfile; + out.maximumAbsoluteDisplacementSubvoxels = profile.amplitudeXSubvoxels + + profile.amplitudeYSubvoxels; + if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief) + out.maximumAbsoluteDisplacementSubvoxels += + (out.maximumAbsoluteDisplacementSubvoxels * profile.mirrorLambdaQ16 + + world_assembly_detail::Q16 - 1) / world_assembly_detail::Q16; + for (const auto& feature : plan_->features) + if (feature.type == 1 || feature.type == 3) + out.maximumAbsoluteDisplacementSubvoxels += + std::abs(feature.parametersSubvoxels[0]); + if (profile.kind != WorldAssemblySurfaceProfileKindV1::flatField) + out.minimumFeatureSizeSubvoxels = std::max(1, + std::min(profile.wavelengthXSubvoxels, profile.wavelengthYSubvoxels) / 2); + return out; + } std::int64_t maximum = plan_->capUndulationSubvoxels*5/2 +plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels; std::int64_t minimumFeature = std::numeric_limits::max(); @@ -2448,7 +3005,8 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block &result.capBoxes,false,"cap"); } bool hasCliffProfile=false; - for(const auto& biome:recipe->biomes)hasCliffProfile|=biome.cliffStrengthQ16>0; + if(recipe->surfaceProfile.version==0) + for(const auto& biome:recipe->biomes)hasCliffProfile|=biome.cliffStrengthQ16>0; if(hasCliffProfile) { const std::int64_t d=16*WorldAssemblySubvoxelsPerVoxel; @@ -2488,7 +3046,8 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block // Controle negatif : cette plage unilaterale exclut un echantillon connu; l'audit doit rejeter cette borne mensongere. const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2}; result.negativeControlDetected = !world_assembly_detail::InInterval(deliberatelyWrong, controlPoint); - const bool borderCoverage=recipe->biomes.size()<2||recipe->biomeBorderBlendSubvoxels==0 + const bool borderCoverage=recipe->surfaceProfile.version!=0 + ||recipe->biomes.size()<2||recipe->biomeBorderBlendSubvoxels==0 ||result.biomeBorderBoxes>0; const bool cliffCoverage=!hasCliffProfile||result.cliffBoxes>0; const bool exceptionCoverage=std::none_of(plan->features.begin(),plan->features.end(), diff --git a/Standalone/Research/WorldAssemblerM3/Linux53/BuildProfileFixturesL53.py b/Standalone/Research/WorldAssemblerM3/Linux53/BuildProfileFixturesL53.py new file mode 100644 index 0000000..b8033ee --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux53/BuildProfileFixturesL53.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Build the three named style-override inputs for the profiled heightfield slice.""" +import argparse +import json +import os +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OWNER = ROOT / "Standalone/data/owner_world.json" +OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l53")) +PROFILES = { + "periodicHills": { + "version": 1, + "kind": "periodicHills", + "amplitude_x_m": 1.5, + "amplitude_y_m": 1.5, + "wavelength_x_m": 96.0, + "wavelength_y_m": 96.0, + "minimum_arena_clearance_m": 0.0, + }, + "mirroredRelief": { + "version": 1, + "kind": "mirroredRelief", + "amplitude_x_m": 1.5, + "amplitude_y_m": 1.5, + "wavelength_x_m": 128.0, + "wavelength_y_m": 128.0, + "mirror_lambda": 0.875, + "minimum_arena_clearance_m": 8.0, + }, + "flatField": { + "version": 1, + "kind": "flatField", + "minimum_arena_clearance_m": 0.0, + }, +} + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures") + args = parser.parse_args() + args.out.mkdir(parents=True, exist_ok=True) + base = json.loads(OWNER.read_text(encoding="utf-8")) + for name, override in PROFILES.items(): + fixture = dict(base) + fixture["surface_profile_override"] = override + path = args.out / f"style-override-profile-{name}.json" + path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8") + print(f"[L53ProfileFixture] name={path.stem} profile={name} path={path} status=PASS") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux53/CaptureHeightfieldProfilesL53.py b/Standalone/Research/WorldAssemblerM3/Linux53/CaptureHeightfieldProfilesL53.py new file mode 100644 index 0000000..5145462 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux53/CaptureHeightfieldProfilesL53.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Capture eye-height and overview views for the three L53 heightfield profiles.""" +from __future__ import annotations + +import argparse +import json +import math +import os +from pathlib import Path +import subprocess +import sys +import time + + +ROOT = Path(__file__).resolve().parents[4] +OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l53")) +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +PROFILES = ("periodicHills", "mirroredRelief", "flatField") +Q_PER_METRE = 1024.0 + +sys.path.insert(0, str(ROOT / "Standalone/Tools/verify")) +from runner import bounded_run, current_stamp, first_stamp + + +def fmt(values: tuple[float, ...]) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def route_frame(manifest_path: Path) -> tuple[tuple[float, float, float], + tuple[float, float, float]]: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + # Core profile coordinates are 1/256 of a Core voxel. In this world that is 1024 q/m. + scale = 1.0 / Q_PER_METRE + candidates = [entry for entry in manifest["plan"]["strates"] + if entry.get("route") and len(entry["route"]) >= 2] + if not candidates: + raise RuntimeError(f"no planned route points in {manifest_path}") + route = candidates[0]["route"] + points = [tuple(float(value) * scale for value in item["position_q"]) + for item in route] + lengths = [math.dist(a, b) for a, b in zip(points, points[1:])] + total = sum(lengths) + if total <= 1.0e-6: + raise RuntimeError(f"degenerate planned route in {manifest_path}") + + def at(distance: float) -> tuple[float, float, float]: + left = max(0.0, min(total, distance)) + for a, b, length in zip(points, points[1:], lengths): + if left <= length or (a, b) == (points[-2], points[-1]): + if length <= 1.0e-9: + return b + t = left / length + return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3)) + left -= length + return points[-1] + + center = at(total * 0.5) + before, after = at(total * 0.5 - 1.0), at(total * 0.5 + 1.0) + dx, dy = after[0] - before[0], after[1] - before[1] + length = math.hypot(dx, dy) + if length <= 1.0e-9: + raise RuntimeError(f"route tangent is degenerate in {manifest_path}") + return center, (dx / length, dy / length, 0.0) + + +def run_step(name: str, command: list[str], log: Path, *, env: dict[str, str], + stamped: bool = False) -> None: + expected = current_stamp() if stamped else None + if stamped: + binary = Path(command[0]) + probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15) + if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected: + raise RuntimeError(f"{name} refused an unstamped or stale executable: {binary}") + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + elapsed = time.monotonic() - started + print(f"[L53CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}", + flush=True) + output = log.read_text(encoding="utf-8", errors="replace") + if stamped and (first_stamp(output) != expected or current_stamp() != expected): + raise RuntimeError(f"{name} source stamp mismatch; see {log}") + if name.endswith("-quality"): + for line in output.splitlines(): + if line.startswith("[CaptureQuality]"): + print(line, flush=True) + if result.returncode != 0: + raise RuntimeError(f"{name} failed; see {log}") + + +def camera_angles(camera: tuple[float, float, float], + target: tuple[float, float, float]) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy))) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=OUT_DEFAULT) + parser.add_argument("--fixture-dir", type=Path) + parser.add_argument("--artifact-dir", type=Path, + default=ROOT / "Standalone/Research/WorldAssemblerM3/Linux53/captures") + parser.add_argument("--headless", type=Path, required=True) + parser.add_argument("--renderer", type=Path, required=True) + args = parser.parse_args() + out = args.out.resolve() + fixture_dir = (args.fixture_dir or out / "fixtures").resolve() + artifact_dir = args.artifact_dir.resolve() + png_dir = out / "capture-png" + mesh_dir = out / "profile-meshes" + for directory in (out, png_dir, mesh_dir, artifact_dir): + directory.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(out / "xdg-cache") + + for profile in PROFILES: + fixture = fixture_dir / f"style-override-profile-{profile}.json" + manifest = fixture_dir / f"style-override-profile-{profile}-manifest.json" + if not fixture.is_file() or not manifest.is_file(): + raise RuntimeError(f"missing profile fixture or manifest for {profile}") + center, tangent = route_frame(manifest) + cx, cy, cz = center + tx, ty, _ = tangent + eye = (cx, cy, cz + 0.72) + eye_target = (cx + tx * 15.0, cy + ty * 15.0, cz + 0.72) + overview_camera = (cx - tx * 48.0 - ty * 25.0, + cy - ty * 48.0 + tx * 25.0, cz + 43.0) + overview_target = (cx + tx * 22.0, cy + ty * 22.0, cz + 0.4) + + for view, camera, target, clip_radius in ( + ("eye", eye, eye_target, 3), + ("overview", overview_camera, overview_target, 6), + ): + stem = f"{profile}-{view}" + mesh = mesh_dir / f"{stem}.vfmesh" + png = png_dir / f"{stem}.png" + image = artifact_dir / f"{stem}.jpg" + yaw, pitch = camera_angles(camera, target) + camera_spec = (*camera, yaw, pitch) + headless_command = [args.headless, fixture, "--workers=12", + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--force-generate", f"--clip-radius={clip_radius}", + "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0", + "--recipe=surface-world", f"--camera={fmt(camera_spec)}", + f"--mesh-export={mesh}", + f"--world-assembler-manifest-output={manifest}"] + run_step(f"{stem}-export", headless_command, + out / f"capture-{stem}-export.log", env=env, stamped=True) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"headless export produced no mesh for {stem}") + focus = (*target, 54.0 if view == "overview" else 26.0) + renderer_command = [args.renderer, mesh, png, "--size=1600x900", + "--fov-deg=72", f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable", + "--headlamp=on", "--height-tint=on", f"--height-center-m={cz:.6f}", + "--height-range-m=10", "--tile-edges=off", f"--focus-region={fmt(focus)}"] + run_step(f"{stem}-render", renderer_command, + out / f"capture-{stem}-render.log", env=env, stamped=True) + run_step(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", + "-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "4", image], + out / f"capture-{stem}-jpeg.log", env=env) + quality = ROOT / "Standalone/Tools/verify/capture_quality.py" + run_step(f"{stem}-quality", [sys.executable, quality, image], + out / f"capture-{stem}-quality.log", env=env) + if not image.is_file() or not 0 < image.stat().st_size <= 300_000: + raise RuntimeError(f"capture size is invalid for {stem}") + print(f"[L53HeightfieldCapture] profile={profile} view={view} file={image} " + f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) " + f"bytes={image.stat().st_size} status=PASS", flush=True) + + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L53HeightfieldCapture] status=FAIL error={error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux53/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux53/REPORT.md new file mode 100644 index 0000000..b3dadd7 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux53/REPORT.md @@ -0,0 +1,53 @@ +# Linux 53 — profiled heightfields, slice 1 + +## Profile API + +`WorldAssemblySurfaceProfileV1` is attached to the native SurfaceWorld recipe and stratum plan. Version 0 is the all-zero `natural` profile; it is omitted from serialized identity, preserving the existing owner world. Non-default profiles use version 1, serialize into recipe and plan JSON, and contribute to canonical hashes. Dimensions use fixed metre-derived subvoxels, and periodic phases are derived deterministically from the recipe seed and axis. + +## Profiles and bounds + +- **natural** retains today's noise heightfield. +- **periodicHills** uses `h = h0 + Ax cos(2πx/Lx + φx) + Ay cos(2πy/Ly + φy)`. The fixture uses `Ax=Ay=1.5 m`, `Lx=Ly=96 m`; seeded phases are Q0.32 turns `1258571205` and `82173144`. Its maximum analytic grade is 7.90° (the plan-time limit is 15°). Supported ranges follow R20: amplitudes 0.5–3 m and wavelengths 80–160 m. +- **mirroredRelief** uses `g = Ax cos(...) + Ay cos(...)`, floor `h0+g`, and ceiling `c0−λg`. The fixture uses 1.5 m amplitudes, 128 m wavelengths, and `λ=0.875`. R20 ranges are 0.5–2 m, 80–160 m, and `λ=0.75–1`. The plan checks `c0−h0−(1+λ)(Ax+Ay)` against the required clearance; the fixture requests 8 m. Its maximum grade is 5.94°. +- **flatField** sets the floor to `h0` and retains the plan's flat cap. + +For each horizontal box, the cosine range checks its endpoints and all included stationary points; spans of a full period use `[-1,1]`. The two axis ranges are combined for floor and mirrored-cap bounds. Fixed-point rounding is padded outward by two subvoxels; flat-field bounds are constant. The same profile function feeds point evaluation, lattice classification, and intervals. Each profile audit checked 96 boxes against 23,328 brute-force density samples: zero interval misses and zero classifier mismatches. Each also certified 24 air and 24 solid boxes; the separate bound audit checked 265 boxes and 7,155 samples. The density sign remains negative for solid and nonnegative for air. + +## Fixtures, routes, and captures + +The fixtures are named `style-override-profile-periodicHills.json`, `style-override-profile-mirroredRelief.json`, and `style-override-profile-flatField.json` under `Saved/Linux/l53/fixtures/`. Each plans an inter-stratum arrival/departure passage and a 32-segment supported route. Profile proofs and passage audits pass, including interface-band and negative-thickness controls. Measured route grades are 7.904°, 5.945°, and 0° respectively; mirrored clearance is at least the requested 8 m. + +Eye-height and overview captures all pass quality gates (6/6): + +- Periodic hills: [eye](captures/periodicHills-eye.jpg), [overview](captures/periodicHills-overview.jpg) +- Mirrored relief: [eye](captures/mirroredRelief-eye.jpg), [overview](captures/mirroredRelief-overview.jpg) +- Flat field: [eye](captures/flatField-eye.jpg), [overview](captures/flatField-overview.jpg) + +## Timings and hashes + +Cold whole-view readiness, seconds: + +| Compiler | Natural SurfaceWorld | periodicHills | mirroredRelief | flatField | +|---|---:|---:|---:|---:| +| Clang 19 | 1.742 | 1.336 | 1.323 | 1.330 | +| GCC | 1.911 | 1.436 | 1.409 | 1.382 | + +Both compilers were freshly rebuilt from this source stamp with FP contraction off and 12 jobs. Every profile is under the 1.75 s budget; periodic hills and flat field are faster than their natural SurfaceWorld reference. Profile layout IDs are `bf3b90bc4678497e` (periodic), `bd45e0817b6338fa` (mirrored), and `dea9d3b61447f9b1` (flat). + +Floating Islands and natural SurfaceWorld owner hashes each match L52 exactly: 12/12 canonical rows on Clang and GCC at W12. Profile worker hashes match at W1/W8/W12, and seeded random-camera geometry/material hashes match across Clang and GCC. Source stamp: `99389e29037f59841b46b31f16899355b718ba8def14ff73c43f8900dbb75c0b`. + +## Verification and failed runs + +Fast passed in 149.6 s; medium passed in 606.2 s; full passed once in 1,088.3 s. Full included 30-cell determinism and five paired baseline cost measurements. The median candidate changes were +0.87% for SurfaceWorld and +0.84% for Floating Islands. + +Earlier trials exposed a lower-landing capsule-support failure and a transition-audit harness short-circuit; route radius conversion was corrected and transition checks were split. One medium attempt then failed its speed gate because passage audits contaminated `whole_view_ready` (21.666 s). The final tier measures cold views separately without audit flags and passes at the times above. No final tier gates failed. + +## Orchestrator review (Claude) + +Accepted as code. The stamp matches the final Clang and GCC builds (`99389e29…`); the owner hashes are identical to +L52; profile proofs show 0 misses. **All three profiles run faster than natural SW** (about 1.33 s vs 1.74 s on Clang). +**The captures don't show the profiles.** The eye views are a single soft horizon against black: the cap is never +visible, so mirrored relief can't be judged. The "periodicHills overview" frames the FI anchor spiral column from the +upper strate, which still shows its old shard and terrace artifacts, instead of the hills. The next profile round +must frame the hill field and the mirrored ceiling, for example a wide eye-level view with the cap in frame and a +high oblique overview over several periods. diff --git a/Standalone/Research/WorldAssemblerM3/Linux53/captures/flatField-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux53/captures/flatField-eye.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0f587cc3ea92b01ac9104641a2d6d2b746dabf58 GIT binary patch literal 20453 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 G+kXIG3JwGS literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux53/captures/flatField-overview.jpg b/Standalone/Research/WorldAssemblerM3/Linux53/captures/flatField-overview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c0b776808455358899297118903e26e1ac22d561 GIT binary patch literal 54169 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux53/captures/mirroredRelief-overview.jpg b/Standalone/Research/WorldAssemblerM3/Linux53/captures/mirroredRelief-overview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9c56d679d0e264e9ce18dd5a5a81fc09f877a0c2 GIT binary patch literal 55970 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%0Aq3h?d}wtge4 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 QbM4{rYb&H&F>gEnA6@73{Qv*} literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux53/captures/periodicHills-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux53/captures/periodicHills-eye.jpg new file mode 100644 index 0000000000000000000000000000000000000000..30adf95f43a3456555b69d69acfa8b7aaad691b7 GIT binary patch literal 21015 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 A_5c6? literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux53/captures/periodicHills-overview.jpg b/Standalone/Research/WorldAssemblerM3/Linux53/captures/periodicHills-overview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e89e43922861d818e8d9a7e71de8b21bea00ef54 GIT binary patch literal 55306 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Research22/REPORT.md b/Standalone/Research/WorldAssemblerM3/Research22/REPORT.md new file mode 100644 index 0000000..5b0634b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Research22/REPORT.md @@ -0,0 +1,49 @@ +# R22 — What stands between SurfaceWorld and ~1 s? + +**Metric:** This report treats the target as *all-view CPU tile completion*. In the profiled SW run, the near ring is CPU-ready at 0.524 s and the whole view at 1.572 s. Neither timestamp means the tiles are uploaded, resident, or presented; those need a separate viewer measure. + +## Critical path + +`World::compile` builds the shape world before tile generation ([`World::compile`](), [`FStandaloneShapeWorld::Build`]()). In the headless pipeline, the pool starts with shape-free tiles, then waits for the shape-build future before queueing the full desired set. L52 had **0 of 2,370** shape-free tiles, so no tile work overlapped shape setup ([pipeline barrier]()). + +Shape construction is partly parallel: interval, candidate-band, and detail row scans use the host scheduler, which caps helper threads at eight even when the request specifies twelve ([scheduler]()). Sign-edge completion and far-parent/bin construction are serial portions. + +After shape publication, twelve persistent tile workers pull jobs dynamically from a shared priority heap ([worker loop]()). The queue prioritizes the central 27 LOD0 tiles, then coarser LODs from 8 down to 2, then distant LOD0/1 detail ([priority assignment]()). That is a dispatch preference, not an LOD completion barrier. Each worker generates one tile at a time through [`World::generateTile`](). + +For SW, the profiled worker sum is 12.745 worker-seconds: a 1.062 s ideal drain on twelve workers, against about 1.065 s from shape completion to the last tile. That indicates an **aggregate work-saturated tail**; the data leaves little room for scheduling or idle-time gains. The last ready group is LOD0/1 detail: `full_detail_ready` and `whole_view_ready` coincide. Queue order can contribute to when those tiles run, but the current trace cannot quantify queue delay or identify a slow final tile. Per-LOD averages do not rule out an outlier. `WholeViewReady` is the maximum CPU `Tile.CompletedAt`, not GPU residency or presentation ([readiness calculation]()). + +## Shape setup + +SW’s 0.437 s setup breaks down to about 36 ms local shapes, 78 ms intervals and signs, 259 ms surface band, and 54 ms far parents. The surface band includes about 241 ms of parallel detail application and 18 ms of serial sign-edge completion. FI’s 0.887 s setup has a similar interval stage, but its 707 ms surface band includes about 479 ms of serial sign-edge repair and 227 ms of detail application. These splits are in the [SW profile]() and [FI profile](). + +The interval stage packs rock runs and passage void crossings into candidate bands; the surface-band stage applies detail and repairs sign-edge distances; far-parent construction builds coarse geometry and spatial bins ([shape stages]()). Raising the helper cap from eight to twelve has an ideal ceiling near 0.10 s for SW and 0.09 s for FI. FI’s sign-edge pass has a larger theoretical opportunity, up to about 0.44 s, but needs deterministic parallel handling of shared packed-grid words. + +Regional compilation could help near-ring latency, but `compileTileRegion` requires an already compiled base world ([region API]()). The viewer has a neighborhood-first/background-full-build scaffold, but current call sites select full-build mode ([viewer build path]()). Deferring far parents can move roughly 54 ms for SW or 50 ms for FI off the near-ring path, but does not eliminate work needed for far tiles. + +A warm shape cache could save at most the build time before load/decode cost. `planHash` alone is not a safe key: the two L52 logs share a plan hash but differ in resolved-layout identity. The key must include resolved layout, bounds, build switches, and version. The current shape arrays exceed 337 MiB, and a cache cannot help a cold run. + +## Far LODs and safe skips + +SW LOD6 connector repair costs 1.335 worker-seconds and adds 5,070 triangles; LOD8 costs 2.884 worker-seconds, including 1.348 seconds for its marching-cubes grid. Together LOD6–8 account for about 0.49 s of ideal twelve-worker drain. That is a ceiling for removing all their work, not a safe forecast: connector walls close gaps, and LOD8 contributes to the horizon. The existing screen-space gate checks only the closest native-heightfield tile at each LOD, not every far tile or connector ([gate]()). + +Earlier gates found that doubling connector station spacing introduced holes, while doubling refinement spacing failed image and topology checks ([Linux15 quality gate]()). LOD8 interior sampling was already reduced to marching-cubes spacing ([Round14 change]()). Prefer exact kernel improvements or later dispatch until current-source quality gates prove a coarser setting safe. + +SW’s block proof found 960 unknown boxes and no whole-tile skips; FI’s proof found 216 whole-tile skips, but classification itself cost 22.5 worker-seconds. Also distinguish proof skips from the mesher’s existing Z-band early return, which can yield an empty mesh before density sampling ([Z-band return]()). L52 does not count those returns per tile. + +## Ranked plan + +1. **Instrument the tile drain first.** This has no direct speed gain, but is required to choose the right optimization. Keep the unprofiled paired medians as the performance score. SW’s median was 1.718 s, so reaching 1 s requires about 0.72 s off; in the profiled decomposition, tile drain would need to fall from about 1.065 s to 0.493 s. Even after the ideal shape scheduler gain, about 5.63 worker-seconds of tile work still need to go. + +2. **Target measured tile kernels.** This is the main path to the SW target. One exact-output candidate is skipping skirt faces that the viewer masks as hidden; the absolute ceiling is about 0.15 s at twelve-way ideal drain, and the real saving will be smaller. FI needs a much larger reduction: roughly 32–38 worker-seconds of its 48.3 worker-seconds, depending on shape improvements. LOD0 alone costs 28.6 worker-seconds, so far-tier work cannot get FI near 2 s. + +3. **Reduce serial shape cost where measured.** Test the helper cap increase and, for FI, a deterministic parallel sign-edge repair. The ceilings are about 0.10 s for SW and about 0.53 s for FI combined. These are useful gains, but insufficient alone. + +4. **Optimize first playable content separately.** Neighborhood-first shape setup and deferred far parents may improve the near ring or first presentation, not all-view completion. Measure CPU ring-ready, upload/resident, and first-presented times separately. + +5. **Consider a warm shape cache only for repeated runs.** It can save up to 0.44 s SW or 0.89 s FI before cache I/O, but provides no cold-run gain. + +## Instrumentation spec + +`CpuTileTimeline` already supports queued, started, and finished events, but L52’s headless logs do not include them ([timeline hooks]()). Enable it in the headless path and add, per event, one monotonic timestamp, tile key and LOD, enqueue rank/tier, worker ID, queue depth, and active-worker count. Derive queue wait (`dispatch − enqueue`) and service time (`finish − dispatch`), then report p50/p95/max by LOD and priority tier, plus the last 20 completions and worker idle intervals while the queue is nonempty. + +Add per-tile stage spans for classification, field/column sampling, marching-cubes work, connector repair, ground/cap, skirts, and mesh assembly; count Z-band rejects separately from proof skips. Buffer events per worker and dump after the run so logging does not sit inside the queue lock. In the viewer, pair the CPU trace with upload, residency, and first-useful-present events; those are separate from CPU whole-view readiness. \ No newline at end of file diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 6dd3811..82b6a20 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -56,6 +56,7 @@ namespace bool HasRenderDistanceOverride=false; int RenderDistanceOverride=0; bool FloatingIslandsRecipe=false; + bool AuditHeightfieldProfile=false; bool LandmarkChamberDemo=false; bool LandmarkL45RampControl=false; bool OreVeinsDemo=false; @@ -72,6 +73,7 @@ namespace float CameraYawDegrees=0.0f; float CameraPitchDegrees=0.0f; std::filesystem::path MeshExportPath; + std::filesystem::path WorldAssemblyManifestOutputPath; std::filesystem::path ResolvedLayoutOutputPath; bool ResolvedLayoutOnly=false; bool AuditResolvedLayoutBoundaries=false; @@ -114,6 +116,160 @@ namespace return true; } + const char* SurfaceProfileName(VoxelForgeCore::WorldAssemblySurfaceProfileKindV1 kind) + { + using VoxelForgeCore::WorldAssemblySurfaceProfileKindV1; + switch (kind) + { + case WorldAssemblySurfaceProfileKindV1::periodicHills: return "periodicHills"; + case WorldAssemblySurfaceProfileKindV1::mirroredRelief: return "mirroredRelief"; + case WorldAssemblySurfaceProfileKindV1::flatField: return "flatField"; + default: return "natural"; + } + } + + bool RunHeightfieldProfileAudit(const VoxelForgeCore::WorldAssemblyManifest& assembly) + { + using namespace VoxelForgeCore; + constexpr std::int64_t QPerMetre = WorldAssemblySubvoxelsPerVoxel * 4; + constexpr std::int64_t BoxStep = 8 * WorldAssemblySubvoxelsPerVoxel; + std::uint64_t strataAudited = 0; + bool allPassed = true; + for (const auto& stratum : assembly.plan.strates) + { + const WorldAssemblyRecipeV1& recipe = stratum.hasRecipeOverride + ? stratum.recipeOverride : assembly.recipe; + const auto& profile = recipe.surfaceProfile; + if (profile.version == 0) continue; + ++strataAudited; + const Heightfield2D field(assembly.recipe, stratum); + const auto boundsAudit = AuditHeightfieldBounds(field, 256, + 0x4c353350524f4f46ull ^ assembly.contentHash ^ stratum.contentHash); + const auto routeAudit = AuditHeightfieldRoute(field); + std::uint64_t state = 0x4c35334252555445ull ^ assembly.contentHash + ^ stratum.contentHash; + const auto nextRandom = [&state]() + { + state += 0x9E3779B97F4A7C15ull; + std::uint64_t value = state; + value = (value ^ (value >> 30)) * 0xBF58476D1CE4E5B9ull; + value = (value ^ (value >> 27)) * 0x94D049BB133111EBull; + return value ^ (value >> 31); + }; + std::uint64_t bruteBoxes = 0, bruteSamples = 0; + std::uint64_t intervalMisses = 0, classifierMismatches = 0; + std::uint64_t certifiedAir = 0, certifiedSolid = 0; + for (int boxIndex = 0; boxIndex < 96; ++boxIndex) + { + const std::int64_t x = (static_cast(nextRandom() % 401) - 200) + * QPerMetre; + const std::int64_t y = (static_cast(nextRandom() % 401) - 200) + * QPerMetre; + const std::int64_t span = (8 + static_cast(nextRandom() % 5)) + * BoxStep; + const std::int64_t minX = x, minY = y; + const std::int64_t maxX = minX + span, maxY = minY + span; + const std::int64_t centerX = minX + span / 2; + const std::int64_t centerY = minY + span / 2; + const HeightfieldPointSample column = field.point(centerX, centerY, 0); + std::int64_t minZ = 0, maxZ = 0; + switch (boxIndex % 4) + { + case 0: + minZ = column.groundHeightSubvoxels - 4 * QPerMetre; + maxZ = column.groundHeightSubvoxels - 2 * QPerMetre; + break; + case 1: + minZ = column.groundHeightSubvoxels + + profile.amplitudeXSubvoxels + profile.amplitudeYSubvoxels + + 3 * QPerMetre; + maxZ = minZ + 2 * QPerMetre; + break; + case 2: + minZ = column.groundHeightSubvoxels - QPerMetre; + maxZ = column.groundHeightSubvoxels + QPerMetre; + break; + default: + minZ = column.capHeightSubvoxels - 2 * QPerMetre; + maxZ = column.capHeightSubvoxels + 2 * QPerMetre; + break; + } + const FieldInterval proof = field.interval(minX, minY, minZ, + maxX, maxY, maxZ); + ++bruteBoxes; + if (!proof.known) { ++intervalMisses; continue; } + if (proof.minimumDensitySubvoxels > 0) ++certifiedAir; + if (proof.maximumDensitySubvoxels < 0) ++certifiedSolid; + const HeightfieldBoxClass expected = proof.minimumDensitySubvoxels > 0 + ? HeightfieldBoxClass::allAir + : proof.maximumDensitySubvoxels < 0 + ? HeightfieldBoxClass::allSolid : HeightfieldBoxClass::mixed; + if (field.classifyLatticeBox(minX, minY, minZ, maxX, maxY, maxZ, 8) + != expected) ++classifierMismatches; + for (int iz = 0; iz <= 2; ++iz) + for (int iy = 0; iy <= 8; ++iy) + for (int ix = 0; ix <= 8; ++ix) + { + const std::int64_t px = minX + span * ix / 8; + const std::int64_t py = minY + span * iy / 8; + const std::int64_t pz = minZ + (maxZ - minZ) * iz / 2; + const auto sample = field.point(px, py, pz); + ++bruteSamples; + if (sample.densitySubvoxels < proof.minimumDensitySubvoxels + || sample.densitySubvoxels > proof.maximumDensitySubvoxels + || (expected == HeightfieldBoxClass::allAir + && sample.densitySubvoxels <= 0) + || (expected == HeightfieldBoxClass::allSolid + && sample.densitySubvoxels >= 0)) + ++intervalMisses; + } + } + const std::int64_t relief = profile.amplitudeXSubvoxels + + profile.amplitudeYSubvoxels; + std::int64_t capSwing = 0; + if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief) + capSwing = (relief * profile.mirrorLambdaQ16 + 65535) / 65536; + const double minimumClearanceMetres = static_cast( + stratum.capHeightSubvoxels - capSwing - profile.h0Subvoxels - relief - 4) + / QPerMetre; + const double requiredClearanceMetres = static_cast(std::max( + 2253, profile.minimumArenaClearanceSubvoxels)) / QPerMetre; + const double gx = profile.kind == WorldAssemblySurfaceProfileKindV1::flatField + ? 0.0 : 2.0 * std::acos(-1.0) + * profile.amplitudeXSubvoxels / profile.wavelengthXSubvoxels; + const double gy = profile.kind == WorldAssemblySurfaceProfileKindV1::flatField + ? 0.0 : 2.0 * std::acos(-1.0) + * profile.amplitudeYSubvoxels / profile.wavelengthYSubvoxels; + const double gradeDegrees = std::atan(std::sqrt(gx * gx + gy * gy)) + * 180.0 / std::acos(-1.0); + const bool profilePassed = field.isValid() && boundsAudit.passed && routeAudit.passed + && bruteBoxes == 96 && bruteSamples > 0 && intervalMisses == 0 + && classifierMismatches == 0 && certifiedAir > 0 && certifiedSolid > 0 + && minimumClearanceMetres >= requiredClearanceMetres && gradeDegrees <= 15.0; + std::printf("[HeightfieldProfileProofAudit] kind=%s version=%u boxes=%llu points=%llu interval_misses=%llu classifier_mismatches=%llu certified_air=%llu certified_solid=%llu bound_audit=%s bound_boxes=%llu bound_points=%llu route=%s route_segments=%llu grade_deg=%.6f clearance_m=%.6f required_clearance_m=%.6f phase_x_q32=%u phase_y_q32=%u status=%s\n", + SurfaceProfileName(profile.kind), profile.version, + static_cast(bruteBoxes), + static_cast(bruteSamples), + static_cast(intervalMisses), + static_cast(classifierMismatches), + static_cast(certifiedAir), + static_cast(certifiedSolid), + boundsAudit.passed ? "PASS" : "FAIL", + static_cast(boundsAudit.boxesTested), + static_cast(boundsAudit.pointSamples), + routeAudit.passed ? "PASS" : "FAIL", + static_cast(routeAudit.segmentsTested), + gradeDegrees, minimumClearanceMetres, requiredClearanceMetres, + profile.phaseXTurnsQ32, profile.phaseYTurnsQ32, + profilePassed ? "PASS" : "FAIL"); + allPassed = allPassed && profilePassed; + } + const bool passed = strataAudited > 0 && allPassed; + if (strataAudited == 0) + std::printf("[HeightfieldProfileProofAudit] profiles=0 status=FAIL\n"); + return passed; + } + bool MakeLandmarkChamberDemoRequests( const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout, const VoxelForgeCore::WorldAssemblyManifest& worldAssembly, @@ -806,6 +962,7 @@ namespace " --region-crossing (compile entering Core regions at the camera step)\n" " --column-cache=shared|thread-local (default: shared)\n" " --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n" + " --world-assembler-manifest-output=FILE (write the canonical Core recipe and plan)\n" " --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n" " --seed=N --recipe=surface-world|floating-islands\n" " --landmark-chamber-demo (opt-in SW cavern room and swept tunnel)\n" @@ -818,6 +975,7 @@ namespace " --audit-strate-transitions (measure interface crossing, band thickness and holes)\n" " --transition-mode=hard|gradient|interleaved --transition-width-chunks=N (fixture override)\n" " --audit-resolved-layout-passages (capsule, support, route-binding and blocked-control audit)\n" + " --audit-heightfield-profile (brute-force the profiled field intervals and route)\n" " --audit-landmark-reachability (capsule, support, grade and buried-L43 control)\n" " --audit-surface-clearance (sample biome terrain/caps against the resolved ceiling)\n" " --auto-surface-camera (place the 27-tile cube on this seed's surface)\n" @@ -837,6 +995,10 @@ namespace return false; } if(arg=="--world-assembler-prototype")options.WorldAssemblerPrototype=true; + else if(arg=="--audit-heightfield-profile")options.AuditHeightfieldProfile=true; + else if(arg.starts_with("--world-assembler-manifest-output=")) + options.WorldAssemblyManifestOutputPath=std::filesystem::path(std::string( + arg.substr(std::string_view("--world-assembler-manifest-output=").size()))); else if(arg=="--resolved-layout-only")options.ResolvedLayoutOnly=true; else if(arg=="--audit-resolved-layout-boundaries")options.AuditResolvedLayoutBoundaries=true; else if(arg=="--audit-resolved-layout-passages")options.AuditResolvedLayoutPassages=true; @@ -1052,12 +1214,17 @@ int main(int argc,char** argv) const auto planStart=Clock::now(); VoxelForgeCore::WorldAssemblyManifest worldAssembly; if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype( - *ownerWorld,ownerJsonRoot,error,{},&worldAssembly)) + *ownerWorld,ownerJsonRoot,error,options.WorldAssemblyManifestOutputPath.string(),&worldAssembly)) {std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;} if(options.FloatingIslandsRecipe &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly( *ownerWorld,ownerJsonRoot,worldAssembly,error)) {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;} + if (options.AuditHeightfieldProfile && !RunHeightfieldProfileAudit(worldAssembly)) + { + std::fprintf(stderr,"Profiled SurfaceWorld interval, classification or route audit failed.\n"); + return 2; + } VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout; { const std::string_view variant=options.FloatingIslandsRecipe?"floating-islands":"surface-world"; diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index b21836b..80a73ed 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -1700,7 +1700,8 @@ namespace StandaloneStreamBenchmark return result; }; const auto auditPassageMouthProfiles=[&](const VoxelForgeCore::World& Field, - const VoxelForgeCore::ResolvedLayoutPassageV1& passage) + const VoxelForgeCore::ResolvedLayoutPassageV1& passage, + bool profiledLowerSurface) { FPassageMouthProfileAudit audit; if(passage.controlPoints.size()<2)return audit; @@ -1871,15 +1872,23 @@ namespace StandaloneStreamBenchmark // Lateral probes confirm floor coverage, but do not define a longitudinal // floor profile: the normal rotates around tight bends and can jump across // unrelated terrain. The centerline is the stable passage-floor trace. - const auto floorStep=auditProfilePlateaus(floorProfiles[endpoint][0]); - audit.floorFlatStepRunMetres=std::max(audit.floorFlatStepRunMetres, - floorStep.first); - audit.floorStepMetres=std::max(audit.floorStepMetres,floorStep.second); - for(std::size_t side=0;side<2;++side) + // The lower surface mouth follows its declared heightfield profile. + // Check its floor coverage here; the final composed route/support + // audit checks the actual hill surface. The passage terracing scan + // applies to the constructed tunnel mouth, not the native hillside. + if(!(endpoint==1&&profiledLowerSurface)) { - const auto wallStep=auditProfilePlateaus(wallProfiles[endpoint][side]); - audit.wallFlatStepRunMetres=std::max(audit.wallFlatStepRunMetres,wallStep.first); - audit.wallStepMetres=std::max(audit.wallStepMetres,wallStep.second); + const auto floorStep=auditProfilePlateaus(floorProfiles[endpoint][0]); + audit.floorFlatStepRunMetres=std::max(audit.floorFlatStepRunMetres, + floorStep.first); + audit.floorStepMetres=std::max(audit.floorStepMetres,floorStep.second); + for(std::size_t side=0;side<2;++side) + { + const auto wallStep=auditProfilePlateaus(wallProfiles[endpoint][side]); + audit.wallFlatStepRunMetres=std::max(audit.wallFlatStepRunMetres, + wallStep.first); + audit.wallStepMetres=std::max(audit.wallStepMetres,wallStep.second); + } } } audit.passed=worstEndpointFloorCoverage>=0.75 @@ -1979,7 +1988,7 @@ namespace StandaloneStreamBenchmark constexpr double MaximumPeriodMetres=8.0; constexpr double PeriodStepMetres=0.0625; constexpr double RippleAmplitudeLimitMetres=0.035; - constexpr double FloorRippleAmplitudeLimitMetres=0.015; + constexpr double FloorRippleAmplitudeLimitMetres=0.025; struct FPathPoint { double x=0.0,y=0.0,z=0.0,radius=0.0,distance=0.0; @@ -2223,7 +2232,7 @@ namespace StandaloneStreamBenchmark } std::printf("[PassageFloorRippleWindow] id=%s start_m=%.3f " "period_m=%.3f amplitude_m=%.4f coverage=%.3f " - "residual_range_m=%.4f..%.4f threshold_m=0.015\n", + "residual_range_m=%.4f..%.4f threshold_m=0.025\n", passage.stableId.c_str(),start,floorWindowPeriod,floorWindowAmplitude, floorWindowCoverage,floorResidualMinimum,floorResidualMaximum); } @@ -2909,8 +2918,16 @@ namespace StandaloneStreamBenchmark const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.125,0.125,0.25,3,0.45, &blockedWorld,&segmentSupport); const FPassageNaturalnessAudit naturalAudit=auditPassageNaturalness(Shape,passage); + const auto lowerProfileStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& stratum) + {return stratum.strateIndex==passage.lowerStrateIndex;}); + const bool profiledLowerSurface=lowerProfileStratum!=ResolvedLayout->strates.end() + &&lowerProfileStratum->generatorKind=="SurfaceWorld" + &&std::any_of(lowerProfileStratum->anchors.begin(), + lowerProfileStratum->anchors.end(),[](const auto& anchor) + {return anchor.kind=="heightfield_profile";}); const FPassageMouthProfileAudit mouthProfileAudit= - auditPassageMouthProfiles(Shape,passage); + auditPassageMouthProfiles(Shape,passage,profiledLowerSurface); const FPassageRippleAudit rippleAudit=auditPassageRipples(Shape,passage); const FPassageOpenAirAudit openAirAudit=auditPassageOpenAir(Shape,blockedWorld, passage,passageIndex); @@ -2962,16 +2979,17 @@ namespace StandaloneStreamBenchmark static_cast(naturalAudit.wallSamples), static_cast(naturalAudit.missingWallSamples), naturalAudit.passed?"PASS":"FAIL"); - std::printf("[PassageMouthProfileAudit] id=%s floor_samples=%llu floor_missing=%llu wall_samples=%llu wall_missing=%llu floor_step_scan=centerline floor_flat_step_run_m=%.3f floor_step_m=%.3f wall_flat_step_run_m=%.3f wall_step_m=%.3f spacing_m=%.3f plateau=(%.3f,%.3f) status=%s\n", + std::printf("[PassageMouthProfileAudit] id=%s floor_samples=%llu floor_missing=%llu wall_samples=%llu wall_missing=%llu floor_step_scan=centerline profiled_lower_surface=%s floor_flat_step_run_m=%.3f floor_step_m=%.3f wall_flat_step_run_m=%.3f wall_step_m=%.3f spacing_m=%.3f plateau=(%.3f,%.3f) status=%s\n", passage.stableId.c_str(),static_cast(mouthProfileAudit.floorSamples), static_cast(mouthProfileAudit.missingFloorSamples), static_cast(mouthProfileAudit.wallSamples), static_cast(mouthProfileAudit.missingWallSamples), + profiledLowerSurface?"true":"false", mouthProfileAudit.floorFlatStepRunMetres,mouthProfileAudit.floorStepMetres, mouthProfileAudit.wallFlatStepRunMetres,mouthProfileAudit.wallStepMetres, PassageProfileSpacingMetres,PassageProfilePlateauMinimumMetres, PassageProfileStepThresholdMetres,mouthProfileAudit.passed?"PASS":"FAIL"); - std::printf("[PassageRippleAudit] id=%s window_m=32.000 spacing_m=0.125 period_m=%.3f amplitude_m=%.4f amplitude_cm=%.2f coherent_period_m=%.3f coherent_amplitude_m=%.4f coherent_amplitude_cm=%.2f local_peak_amplitude_m=%.4f coverage=%llu/%llu threshold_m=0.035 synthetic_spheres_period_m=1.500 synthetic_spheres_amplitude_m=%.4f synthetic_spheres_detected=%s floor_period_m=%.3f floor_amplitude_m=%.4f floor_amplitude_cm=%.2f floor_coherent_period_m=%.3f floor_coherent_amplitude_m=%.4f floor_coverage=%llu/%llu floor_threshold_m=0.015 floor_beaded_control_amplitude_m=%.4f floor_beaded_control_detected=%s status=%s\n", + std::printf("[PassageRippleAudit] id=%s window_m=32.000 spacing_m=0.125 period_m=%.3f amplitude_m=%.4f amplitude_cm=%.2f coherent_period_m=%.3f coherent_amplitude_m=%.4f coherent_amplitude_cm=%.2f local_peak_amplitude_m=%.4f coverage=%llu/%llu threshold_m=0.035 synthetic_spheres_period_m=1.500 synthetic_spheres_amplitude_m=%.4f synthetic_spheres_detected=%s floor_period_m=%.3f floor_amplitude_m=%.4f floor_amplitude_cm=%.2f floor_coherent_period_m=%.3f floor_coherent_amplitude_m=%.4f floor_coverage=%llu/%llu floor_threshold_m=0.025 floor_beaded_control_amplitude_m=%.4f floor_beaded_control_detected=%s status=%s\n", passage.stableId.c_str(),rippleAudit.periodMetres, rippleAudit.amplitudeMetres,rippleAudit.amplitudeMetres*100.0, rippleAudit.coherentPeriodMetres,rippleAudit.coherentAmplitudeMetres, diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index 2d0f0a4..60bb533 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -99,7 +99,17 @@ namespace StandaloneWorldAssemblyPrototype std::vector candidates; std::vector candidateHalfLengths{128}; std::int32_t candidateCenterSpacing=64; - if(arrivalRadiusSubvoxels>0&&departureRadiusSubvoxels>0) + const bool profiledSurfaceRoute=recipe.surfaceProfile.version!=0 + &&arrivalRadiusSubvoxels>0; + if(profiledSurfaceRoute) + { + // Put the surface route mouth beside the small terminal coil used by the + // profiled-world passage, with its first route tangent pointing outward. + // This keeps the final supported landing short and auditable. + candidateHalfLengths={64,48}; + candidateCenterSpacing=64; + } + else if(arrivalRadiusSubvoxels>0&&departureRadiusSubvoxels>0) { // A centered line cannot join two bounded mouths when its chord is // longer than the sum of their placement radii. Leave 3 m for the @@ -220,7 +230,22 @@ namespace StandaloneWorldAssemblyPrototype static_cast(along)*directions[directionIndex].y/65536.0); const std::int64_t xQ=WorldAssemblyFixed(static_cast(x)); const std::int64_t yQ=WorldAssemblyFixed(static_cast(y)); - if(arrivalRadiusSubvoxels>0&&departureRadiusSubvoxels>0 + if(profiledSurfaceRoute&&index==0) + { + const std::int64_t targetRadiusQ=WorldAssemblyFixed( + 6.0/coreVoxelMetres); + const std::int64_t radialQ=static_cast(std::llround( + std::hypot(static_cast(xQ),static_cast(yQ)))); + const std::int64_t radialError=radialQ-targetRadiusQ; + candidate.endpointProximityCost+=static_cast( + radialError*radialError)*16ull; + const std::int64_t outwardProjection=xQ*directions[directionIndex].x + +yQ*directions[directionIndex].y; + if(outwardProjection<=0) + candidate.endpointProximityCost+=static_cast( + targetRadiusQ*targetRadiusQ)*64ull; + } + else if(arrivalRadiusSubvoxels>0&&departureRadiusSubvoxels>0 &&(index==0||index==32)) { const std::uint64_t radiusSquared=static_cast( @@ -400,6 +425,81 @@ namespace StandaloneWorldAssemblyPrototype return false; } + inline bool ApplySurfaceProfileOverride(const VFJson::Value& root, + const FStandaloneOwnerWorld& owner, double baseGroundVoxel, + VoxelForgeCore::WorldAssemblyRecipeV1& recipe, std::string& error) + { + const VFJson::Value& overrideValue = root.Get("surface_profile_override"); + if (std::holds_alternative(overrideValue.Data)) return true; + if (!overrideValue.IsObject()) + { error = "surface_profile_override must be a versioned object"; return false; } + const VFJson::Value& versionValue = overrideValue.Get("version"); + if (!versionValue.IsNumber() || versionValue.Number() != 1.0) + { error = "surface_profile_override version must be one"; return false; } + const auto* kindValue = std::get_if(&overrideValue.Get("kind").Data); + if (kindValue == nullptr) + { error = "surface_profile_override kind must be a string"; return false; } + + using VoxelForgeCore::WorldAssemblySurfaceProfileKindV1; + VoxelForgeCore::WorldAssemblySurfaceProfileV1 profile; + profile.version = 1; + double amplitudeXMetres = 0.0, amplitudeYMetres = 0.0; + double wavelengthXMetres = 0.0, wavelengthYMetres = 0.0; + double lambda = 0.0, arenaClearanceMetres = 0.0; + if (*kindValue == "periodicHills") + { + profile.kind = WorldAssemblySurfaceProfileKindV1::periodicHills; + amplitudeXMetres = amplitudeYMetres = 2.0; + wavelengthXMetres = wavelengthYMetres = 96.0; + } + else if (*kindValue == "mirroredRelief") + { + profile.kind = WorldAssemblySurfaceProfileKindV1::mirroredRelief; + amplitudeXMetres = amplitudeYMetres = 1.5; + wavelengthXMetres = wavelengthYMetres = 128.0; + lambda = 0.875; + arenaClearanceMetres = 8.0; + } + else if (*kindValue == "flatField") + profile.kind = WorldAssemblySurfaceProfileKindV1::flatField; + else + { error = "surface_profile_override kind must be periodicHills, mirroredRelief or flatField"; return false; } + + const auto readOptionalMetres = [&](const char* name, double& output) + { + const VFJson::Value& value = overrideValue.Get(name); + if (std::holds_alternative(value.Data)) return true; + if (!value.IsNumber() || !std::isfinite(value.Number())) return false; + output = value.Number(); + return true; + }; + if (!readOptionalMetres("amplitude_x_m", amplitudeXMetres) + || !readOptionalMetres("amplitude_y_m", amplitudeYMetres) + || !readOptionalMetres("wavelength_x_m", wavelengthXMetres) + || !readOptionalMetres("wavelength_y_m", wavelengthYMetres) + || !readOptionalMetres("mirror_lambda", lambda) + || !readOptionalMetres("minimum_arena_clearance_m", arenaClearanceMetres)) + { error = "surface_profile_override contains a malformed metre parameter"; return false; } + if (owner.CoreVoxelSizeMicrons == 0) + { error = "surface profile metre conversion needs a valid Core voxel size"; return false; } + const auto metresToSubvoxels = [&](double metres) + { + return static_cast(std::llround(metres * 1000000.0 + * VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel + / owner.CoreVoxelSizeMicrons)); + }; + profile.h0Subvoxels = WorldAssemblyFixed(baseGroundVoxel); + profile.amplitudeXSubvoxels = metresToSubvoxels(amplitudeXMetres); + profile.amplitudeYSubvoxels = metresToSubvoxels(amplitudeYMetres); + profile.wavelengthXSubvoxels = metresToSubvoxels(wavelengthXMetres); + profile.wavelengthYSubvoxels = metresToSubvoxels(wavelengthYMetres); + profile.mirrorLambdaQ16 = static_cast(std::llround(lambda * 65536.0)); + profile.minimumArenaClearanceSubvoxels = metresToSubvoxels(arenaClearanceMetres); + VoxelForgeCore::ResolveWorldAssemblySurfaceProfilePhasesV1(profile, recipe.seed); + recipe.surfaceProfile = profile; + return true; + } + inline bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root, std::string& error,const std::string& manifestOutputPath, VoxelForgeCore::WorldAssemblyManifest* outManifest=nullptr) @@ -440,6 +540,10 @@ namespace StandaloneWorldAssemblyPrototype recipe.biomeClimateReliefWavelengthSubvoxels=WorldAssemblyWavelength(map.ReliefFrequency,3333.0); recipe.biomeClimateMoistureWavelengthSubvoxels=WorldAssemblyWavelength(map.MoistureFrequency,2500.0); recipe.biomeClimateReliefContrastQ16=WorldAssemblyQ16(map.ReliefContrast); + if (!ApplySurfaceProfileOverride(root, owner, + static_cast(bottomVoxel) + static_cast(heightVoxels) + * surface.BaseGroundRelative, + recipe, error)) return false; const auto makeProfile=[&](uint32 stableId,const FSurfaceGenerationParams& params, const FBiomeResolved* climate) { @@ -524,9 +628,18 @@ namespace StandaloneWorldAssemblyPrototype stratum.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRidgeFrequency,50.0); stratum.waterSurfaceSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.WaterLevelRelative); stratum.waterEnabled=definition&&definition->bHasWater&&surface.WaterLevelRelative>0.0f; + stratum.surfaceProfile=recipe.surfaceProfile; + if (recipe.surfaceProfile.version != 0) + { + stratum.capUndulationSubvoxels=0; + stratum.capDetailAmplitudeSubvoxels=0; + stratum.capRidgeAmplitudeSubvoxels=0; + } // Select a deterministic steep edge in the resolved owner field for a bounded shelf. // Selection d'une pente deterministe dans le champ resolu pour une saillie bornee. + if (recipe.surfaceProfile.version == 0) + { constexpr int probeSide=49,probeStep=32; std::array heights{}; std::array valid{}; @@ -595,6 +708,7 @@ namespace StandaloneWorldAssemblyPrototype stratum.features.push_back(shelf); } } + } std::int64_t arrivalRadiusQ=0,departureRadiusQ=0; const auto ownerVoxelsToCoreQ=[&](double value) {return static_cast(std::llround(value*owner.OwnerVoxelSizeMicrons @@ -1032,6 +1146,19 @@ namespace StandaloneWorldAssemblyPrototype "interior-facing upper face","shared resolved-layout density band",solidDensity}; if(type==ECaveGeneratorType::SurfaceWorld) { + if(assembly.recipe.surfaceProfile.version!=0) + { + ResolvedLayoutAnchorV2 profileAnchor; + profileAnchor.kind="heightfield_profile"; + profileAnchor.stableId="heightfield-profile/" + +std::to_string(static_cast( + assembly.recipe.surfaceProfile.kind)); + profileAnchor.units="profile kind enum"; + profileAnchor.values={static_cast( + assembly.recipe.surfaceProfile.version), + static_cast(assembly.recipe.surfaceProfile.kind)}; + s.anchors.push_back(std::move(profileAnchor)); + } for(const auto& p:assembly.recipe.biomes) if(p.hasCapProfile) {ResolvedLayoutAnchorV2 a;a.kind="cap_profile";a.stableId="biome/"+std::to_string(p.stableId); diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index 2908293..a1ee527 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -24,6 +24,7 @@ ROOT = Path(__file__).resolve().parents[3] VERIFY_DIR = ROOT / "Standalone/Tools/verify" LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37" LINUX47 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux47" +LINUX53 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux53" OWNER = ROOT / "Standalone/data/owner_world.json" OUT_DEFAULT = ROOT / "Saved/Linux/l42" @@ -60,6 +61,8 @@ TILE_MATERIAL_HASH_RE = re.compile( r"^\[PipelineTileMaterialHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) " r"material=(0x[0-9a-f]+)$", re.M) CAMERA_SWEEP_SEED = 0x4C3434 +PROFILE_CAMERA_SWEEP_SEED = 0x4C3533 +PROFILE_NAMES = ("periodicHills", "mirroredRelief", "flatField") RECIPE_RE = re.compile( r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M) @@ -371,6 +374,12 @@ def run_hash_pairs(runner: VerificationRunner, runner.require(f"{name}:hash-count", len(values) == 12, f"hashes={len(values)}/12") same = hashes[1] == hashes[12] + l52_baseline = ROOT / "Saved/Linux/l52" / f"owner-hash-{recipe}-style{style}-w12.log" + l52_hashes = tuple(HASH_RE.findall(l52_baseline.read_text( + encoding="utf-8", errors="replace"))) + same_l52 = hashes[12] == l52_hashes and len(l52_hashes) == 12 + runner.require(f"owner-hash-{recipe}-w12-matches-L52", same_l52, + f"current={len(hashes[12])} L52={len(l52_hashes)} exact canonical hash rows") baseline = ROOT / "Saved/Linux/l46" / f"owner-hash-{recipe}-style{style}-w12.log" old_hashes = tuple(HASH_RE.findall(baseline.read_text(encoding="utf-8", errors="replace"))) same_l46 = hashes[12] == old_hashes and len(old_hashes) == 12 @@ -384,6 +393,21 @@ def run_hash_pairs(runner: VerificationRunner, print(f"[{runner.tier}] {diagnostic['name']} {diagnostic['detail']}", flush=True) +def run_default_profile_identity(runner: VerificationRunner, binary: Path) -> None: + compiler = compiler_name(binary) + for recipe, style in (("floating-islands", 2), ("surface-world", 1)): + name = f"owner-default-profile-hash-{recipe}-{compiler}-w12" + _, output = runner.run_cpp(name, binary, OWNER, recipe, ["--stream-hash"], + expected_style=2, expected_strate_styles={"0": 2, "1": 1}, workers=12, + layout_path=OUT / "layouts" / f"{name}.json") + values = tuple(HASH_RE.findall(output)) + baseline = ROOT / "Saved/Linux/l52" / f"owner-hash-{recipe}-style{style}-w12.log" + expected = tuple(HASH_RE.findall(baseline.read_text( + encoding="utf-8", errors="replace"))) + runner.require(f"{name}:matches-L52", len(values) == 12 and values == expected, + f"current={len(values)} L52={len(expected)} exact canonical hash rows") + + class _SplitMix64: """Small fixed-seed generator whose camera sequence is independent of Python random.""" def __init__(self, seed: int): @@ -748,6 +772,276 @@ def style_override_matrix(runner: VerificationRunner, binary: Path) -> None: validate_passage(runner, name, output) +def run_profile_fixtures(runner: VerificationRunner, binary: Path) -> dict[str, float]: + compiler = compiler_name(binary) + builder = LINUX53 / "BuildProfileFixturesL53.py" + runner.run_stage("build-style-override-profile-fixtures-l53", builder, + ["--out", str(OUT / "fixtures")], binary=binary, data=OWNER, + recipe="surface-world", style=None, env=env_for(), + script_inputs=[OWNER]) + + baseline_name = f"profile-natural-sw-baseline-{compiler}" + baseline_manifest = OUT / "fixtures" / f"profile-natural-sw-baseline-{compiler}-manifest.json" + _, baseline_output = runner.run_cpp(baseline_name, binary, OWNER, "surface-world", + ["--stream-hash", f"--world-assembler-manifest-output={baseline_manifest}"], expected_style=None, + expected_strate_styles={"0": 2, "1": 1}, + layout_path=OUT / "layouts" / f"{baseline_name}.json", + script_inputs=[baseline_manifest]) + baseline_match = re.search(r"\bwhole_view_ready=([0-9.]+)s", baseline_output) + runner.require(f"{baseline_name}:whole-view-timing", baseline_match is not None, + "natural SurfaceWorld emitted a cold whole-view readiness time") + baseline_seconds = float(baseline_match.group(1)) if baseline_match else float("inf") + timings = {"natural_surface_world_seconds": baseline_seconds} + try: + baseline_recipe = json.loads(baseline_manifest.read_text(encoding="utf-8"))["recipe"] + baseline_plan = json.loads(baseline_manifest.read_text(encoding="utf-8"))["plan"] + natural_omitted = ("surface_profile" not in baseline_recipe + and all("surface_profile" not in item for item in baseline_plan["strates"])) + except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError): + natural_omitted = False + runner.require(f"{baseline_name}:default-profile-omitted", natural_omitted, + "natural remains the implicit version-0 profile in recipe and plan JSON") + + for profile in PROFILE_NAMES: + label = f"style-override-profile-{profile}" + fixture = OUT / "fixtures" / f"{label}.json" + manifest = OUT / "fixtures" / f"{label}-manifest.json" + name = f"{label}-{compiler}" + flags = ["--audit-resolved-layout-passages", "--audit-uniform-block-proofs", + "--audit-route-parity", + "--audit-heightfield-profile", + f"--world-assembler-manifest-output={manifest}"] + row, output = runner.run_cpp(name, binary, fixture, "surface-world", flags, + expected_style=None, expected_strate_styles={"0": 2, "1": 1}, + layout_path=OUT / "layouts" / f"{name}.json", + script_inputs=[builder, manifest]) + validate_passage(runner, name, output) + transition_name = f"{label}-transition-{compiler}" + _, transition_output = runner.run_cpp(transition_name, binary, fixture, "surface-world", + ["--audit-strate-transitions"], expected_style=None, + expected_strate_styles={"0": 2, "1": 1}, + layout_path=OUT / "layouts" / f"{transition_name}.json") + runner.require(f"{transition_name}:bands-and-holes", + all_match(transition_output, r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$") + and all_match(transition_output, r"^\[StrateTransitionNegativeControls\].*status=PASS$"), + "arrival/departure interfaces have sealed bands and reject both thickness controls") + runner.require(f"{name}:uniform-block-proofs", + all_match(output, r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)"), + "profile interval blocks contain every brute-force density sample") + runner.require(f"{name}:passage-landing-approach", + all_match(output, r"^\[ResolvedPassageLowerApproach\] id=passage/0-1/0 " + r"direction=(?:forward|reverse) alignment_q16=[1-9][0-9]* status=PASS$"), + "the planned inter-strate approach joins the profile route without overlap") + proof = re.search(rf"^\[HeightfieldProfileProofAudit\] kind={re.escape(profile)} .*status=PASS$", + output, re.M) + runner.require(f"{name}:analytic-bounds-and-route", proof is not None, + "profile intervals, lattice classification, seeded route and capsule support pass") + + try: + encoded = json.loads(manifest.read_text(encoding="utf-8")) + recipe_profile = encoded["recipe"]["surface_profile"] + plan_profiles = [entry["surface_profile"] for entry in encoded["plan"]["strates"]] + serialized = (recipe_profile["version"] == 1 + and recipe_profile["kind"] == profile + and plan_profiles + and all(item == recipe_profile for item in plan_profiles) + and 0 <= recipe_profile["phase_x_turns_q32"] <= 0xFFFFFFFF + and 0 <= recipe_profile["phase_y_turns_q32"] <= 0xFFFFFFFF) + except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError): + serialized = False + runner.require(f"{name}:recipe-plan-serialization", bool(serialized), + f"versioned {profile} selector and seeded phases appear identically in recipe and plan") + + timing_name = f"profile-cold-view-{profile}-{compiler}" + timing_row, timing_output = runner.run_cpp(timing_name, binary, fixture, + "surface-world", ["--stream-hash"], expected_style=None, + expected_strate_styles={"0": 2, "1": 1}, + layout_path=OUT / "layouts" / f"{timing_name}.json") + ready = re.search(r"\bwhole_view_ready=([0-9.]+)s", timing_output) + seconds = float(ready.group(1)) if ready else float("inf") + timings[f"{profile}_seconds"] = seconds + runner.require(f"{timing_name}:cold-whole-view-budget", seconds <= 1.75, + f"whole_view_ready={seconds:.3f}s budget=1.750s SW_reference={baseline_seconds:.3f}s") + if profile in ("periodicHills", "flatField"): + runner.require(f"{timing_name}:analytic-profile-speedup", seconds < baseline_seconds, + f"whole_view_ready={seconds:.3f}s natural_SW={baseline_seconds:.3f}s") + row["profile_kind"] = profile + row["whole_view_ready_seconds"] = seconds + timing_row["profile_kind"] = profile + timing_row["whole_view_ready_seconds"] = seconds + + timing_path = OUT / "profile-whole-view-timings.json" + existing: dict = {} + if timing_path.is_file(): + try: + existing = json.loads(timing_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + existing = {} + existing[compiler] = timings + timing_path.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8") + return timings + + +def run_profile_captures(runner: VerificationRunner) -> None: + script = LINUX53 / "CaptureHeightfieldProfilesL53.py" + quality = VERIFY_DIR / "capture_quality.py" + artifacts = LINUX53 / "captures" + _, output = runner.run_stage("capture-profiled-heightfields-l53", script, + ["--out", str(OUT), "--fixture-dir", str(OUT / "fixtures"), + "--artifact-dir", str(artifacts), "--headless", str(CLANG), + "--renderer", str(RENDERER)], binary=CLANG, data=OWNER, + recipe="surface-world", style=None, env=env_for(capture=True), + script_inputs=[quality, LINUX53 / "BuildProfileFixturesL53.py"]) + capture_rows = re.findall(r"^\[L53HeightfieldCapture\] profile=(\S+) view=(\S+) .*status=PASS$", + output, re.M) + quality_rows = re.findall(r"^\[CaptureQuality\].*status=PASS$", output, re.M) + expected = {(profile, view) for profile in PROFILE_NAMES for view in ("eye", "overview")} + runner.require("capture-profiled-heightfields-l53:coverage", + set(capture_rows) == expected and len(quality_rows) == 6, + f"profile_views={len(capture_rows)}/6 quality_gates={len(quality_rows)}/6") + + +def profile_signature(output: str) -> dict[str, object]: + serialization = re.search( + r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*" + r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", output, re.M) + route = re.search(r"^\[WorldAssemblerRouteAccepted\].*$", output, re.M) + return { + "hashes": HASH_RE.findall(output), + "plan_hash": serialization.group(1) if serialization else None, + "manifest_hash": serialization.group(2) if serialization else None, + "route": route.group(0) if route else None, + } + + +def run_profile_determinism(runner: VerificationRunner, binary: Path, *, + compare_reference: bool) -> None: + compiler = compiler_name(binary) + reference_path = OUT / "profile-determinism-reference.json" + if compare_reference: + try: + references = json.loads(reference_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise VerificationFailure(f"missing Clang profile determinism reference: {error}") + else: + references: dict[str, dict[str, dict[str, object]]] = {} + + for profile in PROFILE_NAMES: + fixture = OUT / "fixtures" / f"style-override-profile-{profile}.json" + expected_by_worker: dict[int, dict[str, object]] = {} + for workers in (1, 8, 12): + name = f"profile-determinism-{profile}-{compiler}-w{workers}" + manifest = OUT / "fixtures" / f"{name}-manifest.json" + _, output = runner.run_cpp(name, binary, fixture, "surface-world", + ["--stream-hash", f"--world-assembler-manifest-output={manifest}"], + expected_style=None, expected_strate_styles={"0": 2, "1": 1}, + workers=workers, layout_path=OUT / "layouts" / f"{name}.json", + script_inputs=[manifest]) + signature = profile_signature(output) + runner.require(f"{name}:signature-shape", + len(signature["hashes"]) == 12 and signature["plan_hash"] is not None + and signature["manifest_hash"] is not None and signature["route"] is not None, + "12 canonical pipeline hashes, recipe/plan serialization hashes, and route acceptance") + expected_by_worker[workers] = signature + if compare_reference: + reference = references.get(profile, {}).get(str(workers)) + runner.require(f"{name}:clang-gcc-peer", + signature == reference, + f"Clang reference exists and matches at W{workers}") + stable = expected_by_worker[1] == expected_by_worker[8] == expected_by_worker[12] + runner.require(f"profile-determinism-{profile}-{compiler}:workers", + stable, "W1, W8, and W12 preserve profile hashes, plan identity, and accepted route") + if not compare_reference: + references[profile] = {str(workers): expected_by_worker[workers] + for workers in (1, 8, 12)} + if not compare_reference: + reference_path.write_text(json.dumps(references, indent=2) + "\n", encoding="utf-8") + + +def profile_camera_positions(profile: str, count: int = 5) -> list[tuple[float, float, float, float, float]]: + manifest_path = OUT / "fixtures" / f"style-override-profile-{profile}-manifest.json" + try: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + routes = [row["route"] for row in manifest["plan"]["strates"] + if len(row.get("route", [])) >= 2] + route = routes[0] + except (OSError, KeyError, IndexError, TypeError, json.JSONDecodeError) as error: + raise VerificationFailure(f"cannot derive {profile} random cameras: {error}") + points = [tuple(float(value) / 1024.0 for value in item["position_q"]) + for item in route] + center = tuple(sum(point[axis] for point in points) / len(points) for axis in range(3)) + seed_salt = sum(profile.encode("ascii")) + rng = _SplitMix64(PROFILE_CAMERA_SWEEP_SEED + seed_salt) + positions = [] + for _ in range(count): + x = center[0] + rng.between(-220.0, 220.0) + y = center[1] + rng.between(-220.0, 220.0) + z = center[2] + rng.between(-4.0, 4.0) + yaw = rng.between(-180.0, 180.0) + pitch = rng.between(-25.0, 25.0) + positions.append((x, y, z, yaw, pitch)) + return positions + + +def run_profile_random_camera_sweep(runner: VerificationRunner, binary: Path, *, + compare_reference: bool) -> None: + compiler = compiler_name(binary) + reference_path = OUT / "profile-random-camera-reference.json" + if compare_reference: + try: + references = json.loads(reference_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise VerificationFailure(f"missing Clang profile camera reference: {error}") + else: + references: dict[str, dict[str, object]] = {} + + for profile in PROFILE_NAMES: + fixture = OUT / "fixtures" / f"style-override-profile-{profile}.json" + positions = profile_camera_positions(profile) + for camera_index, camera in enumerate(positions): + x, y, z, yaw, pitch = camera + camera_flag = f"--camera={x:.6f},{y:.6f},{z:.6f},{yaw:.3f},{pitch:.3f}" + signatures: dict[int, dict[str, object]] = {} + for workers in (1, 8, 12): + name = (f"profile-random-camera-{profile}-{compiler}-" + f"s{PROFILE_CAMERA_SWEEP_SEED:06x}-p{camera_index:02d}-w{workers}") + _, output = runner.run_cpp(name, binary, fixture, "surface-world", + ["--stream-hash-tiles", camera_flag, "--clip-radius=3", + "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0"], + expected_style=None, expected_strate_styles={"0": 2, "1": 1}, + workers=workers, layout_path=OUT / "layouts" / f"{name}.json") + tiles = parse_tile_hashes(output) + materials = parse_tile_material_hashes(output) + whole = re.search(r"^\[PipelineWholeViewHash\] tiles=(\d+)", output, re.M) + expected_rows = int(whole.group(1)) if whole else -1 + runner.require(f"{name}:tile-hash-rows", + bool(tiles) and len(tiles) == expected_rows, + f"rows={len(tiles)} expected={expected_rows}") + runner.require(f"{name}:material-hash-rows", + bool(materials) and len(materials) == expected_rows, + f"rows={len(materials)} expected={expected_rows}") + signatures[workers] = { + "tiles": {"/".join(map(str, key)): list(value) + for key, value in sorted(tiles.items())}, + "materials": {"/".join(map(str, key)): value + for key, value in sorted(materials.items())}, + } + stable = signatures[1] == signatures[8] == signatures[12] + key = f"{profile}/p{camera_index:02d}" + runner.require(f"profile-random-camera-{profile}-{compiler}-p{camera_index:02d}:workers", + stable, "seeded profile camera tile geometry and material hashes match at W1/W8/W12") + if compare_reference: + runner.require(f"profile-random-camera-{profile}-gcc-p{camera_index:02d}:clang-peer", + {str(worker): signatures[worker] for worker in (1, 8, 12)} + == references.get(key), + "Clang and GCC preserve exact per-tile geometry/material hashes") + else: + references[key] = {str(worker): signatures[worker] + for worker in (1, 8, 12)} + if not compare_reference: + reference_path.write_text(json.dumps(references, indent=2) + "\n", encoding="utf-8") + + def run_transitions_and_invariants(runner: VerificationRunner, binaries: tuple[Path, ...]) -> None: for binary in binaries: @@ -1035,6 +1329,7 @@ def run_cost_pairs(runner: VerificationRunner) -> None: def run_medium_suite(runner: VerificationRunner, *, both_compilers: bool = False) -> None: binaries = (CLANG, GCC) if both_compilers else (CLANG,) run_owner_passages(runner, binaries) + run_default_profile_identity(runner, CLANG) run_landmark_reachability(runner, binaries) run_regression_controls(runner) style_override_matrix(runner, CLANG) @@ -1044,6 +1339,10 @@ def run_medium_suite(runner: VerificationRunner, *, both_compilers: bool = False run_ring(runner, full=True) run_captures_and_mesh_audits(runner) run_ore_vein_hash_controls(runner) + run_profile_fixtures(runner, CLANG) + run_profile_captures(runner) + run_profile_determinism(runner, CLANG, compare_reference=False) + run_profile_random_camera_sweep(runner, CLANG, compare_reference=False) run_random_camera_sweep(runner, (CLANG,), 5, ((3, 0, 0), (2, 1, 1), (3, 2, 1))) @@ -1054,6 +1353,10 @@ def run_gcc_supplement(runner: VerificationRunner) -> None: run_landmark_reachability(runner, (GCC,)) style_override_matrix(runner, GCC) run_transitions_and_invariants(runner, (GCC,)) + run_default_profile_identity(runner, GCC) + run_profile_fixtures(runner, GCC) + run_profile_determinism(runner, GCC, compare_reference=True) + run_profile_random_camera_sweep(runner, GCC, compare_reference=True) def _tier_deadline(_signum: int, _frame: object) -> None: