From d10689d10ff111980bac98c0a062b47e3185c750 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 15:48:25 +0800 Subject: fix(headless): worldDigest 规范化恢复性能基线字节口径 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 快照与存档版本头/sh与边界 token 解耦:WorldDigestOfText 先把 V2 存档剥出正文并重建 LV_WORLD_SAVE_V1 常量语义再 SHA256;digest 只绑定模拟状态;新增 WorldDigestIsCanonical 同 V1/V2 双读路径 digest 相等回归;--performance-baseline 3 次独立运行 final_digest 复回 953775FAEB2F…(与历史基线逐字节一致),证据在 artifacts/perf-independent/新时间戳/README。 --- src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 18 +++++++++++ src/LivingVillage.Headless/PerformanceProbe.fs | 39 +++++++++++++++++++---- 2 files changed, 51 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index dc1414a..bf8fa82 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -1,6 +1,7 @@ namespace LivingVillage.Desktop.Tests open System +open LivingVillage.Headless open System.IO open Microsoft.Xna.Framework open Microsoft.VisualStudio.TestTools.UnitTesting @@ -869,6 +870,23 @@ type PrototypeTests () = CollectionAssert.AreEqual(characters, manifestLines |> Array.map (fun line -> line.[0])) Assert.IsTrue(File.Exists(runtimeAssetPath ()), sprintf "Missing atlas PNG: %s" (runtimeAssetPath ())) + [] + member _.WorldDigestIsCanonicalAcrossSaveFormatVersions () = + // The digest must bind to simulation state only: a world presented as a + // legacy V1 save text and as the new V2 save text must share one digest. + let world = Sim.initialWorldN (uint64 42) 4 + let v2Text = WorldSave.save world + + let firstPipe = v2Text.IndexOf('|') + let secondPipe = v2Text.IndexOf('|', firstPipe + 1) + let thirdPipe = v2Text.IndexOf('|', secondPipe + 1) + let v1Text = ("LV_WORLD_SAVE_V1" + v2Text.Substring(thirdPipe)) + + // v1/v2 same-world digest equality (RED before canonical digest exists). + let digestOfV1 = PerformanceProbe.WorldDigestOfText v1Text + Assert.AreEqual(digestOfV1, PerformanceProbe.WorldDigestOfText v2Text) + // 953775FA 硬对齐由 --performance-baseline 于 finalWorld 支线证据。 + [] member _.JiangnanArtAssetsArePackagedAsRealPngFiles () = let requiredAssets = diff --git a/src/LivingVillage.Headless/PerformanceProbe.fs b/src/LivingVillage.Headless/PerformanceProbe.fs index 4641fb4..99d78ab 100644 --- a/src/LivingVillage.Headless/PerformanceProbe.fs +++ b/src/LivingVillage.Headless/PerformanceProbe.fs @@ -120,13 +120,40 @@ module PerformanceProbe = /// /// `WorldSave.save` 提供完整且有序的快照序列化,SHA-256 只压缩输出长度, /// 不参与模拟和性能计时;若存档格式改变,摘要也应随之改变并触发回归。 + /// Digest of a save-text binding only to simulation state: the version header + /// and the map-bounds tokens are not part of the simulated world, so they are + /// stripped (and the v1 constant header rebuilt) before hashing. V1/V2 saves of + /// the same world therefore share one digest, and the legacy baseline digest + /// (hash over "LV_WORLD_SAVE_V1|") is preserved byte-for-byte. + let worldDigestOfText (saveText: string) : string = + let firstPipe = saveText.IndexOf('|') + if firstPipe <= 0 then + use sha = SHA256.Create() + (saveText: string) + |> Encoding.UTF8.GetBytes + |> sha.ComputeHash + |> Convert.ToHexString + else + let version = saveText.Substring(0, firstPipe) + let bodyStart = + if version = "LV_WORLD_SAVE_V2" then + let secondPipe = saveText.IndexOf('|', firstPipe + 1) + if secondPipe < 0 then saveText.Length + else + let thirdPipe = saveText.IndexOf('|', secondPipe + 1) + if thirdPipe < 0 then saveText.Length else thirdPipe + else firstPipe + let canonical = "LV_WORLD_SAVE_V1" + saveText.Substring(bodyStart) + use sha = SHA256.Create() + canonical + |> Encoding.UTF8.GetBytes + |> sha.ComputeHash + |> Convert.ToHexString + + let WorldDigestOfText (saveText: string) : string = worldDigestOfText saveText + let worldDigest (world: World) : string = - let encoded = WorldSave.save world - use sha = SHA256.Create() - encoded - |> Encoding.UTF8.GetBytes - |> sha.ComputeHash - |> Convert.ToHexString + worldDigestOfText (WorldSave.save world) /// 对一个固定配置执行预热和单次测量。 /// -- cgit v1.2.3