# 维护说明 结论先行:主分支保持 0 警告 0 错误 + Desktop 86/86、Kernel 79/79 全绿;性能基线 final_digest 固定 953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C。 本文记录模块职责、不变量与逐字可复制的验收命令。 ## 模块职责 - `src/LivingVillage.Kernel/`:纯确定性模拟(需求/关系/谣言/年鉴/交易),无渲染依赖。 - `Sim.fs`:世界结构、`Sim.step` 主推进、对话/交易/谣言决策、地图边界 `configureBounds`(默认 64×48,可到 512×384)。 - `WorldSave.fs`:存档序列化。V2 头携带地图宽高,V1 旧档读入自动回退 64×48。 - `Personality.fs / Interaction.fs(fsm) / M6*.fs`:NPC 行为状态机与里程碑数据。 - `src/LivingVillage.Desktop/`:MonoGame 渲染与交互层。 - `VillageArt.fs`:图集加载与世界/人物/室内绘制;CC0 覆盖层(LV_ASSET_PACK=cc0)。 - `Interaction.fs`:M5 视图与情境解析(FloatingPrompt / panelLines / 情境提示)。 - `M6Presentation.fs`:昼夜光照纯函数(lightingBlend)与渲染档(Blend/LanternGlow)。 - `TitleScreen.fs / Game.fs`:标题布局与主循环;LV Autos(sample / flow / record / map tour)。 - `ProceduralMap.fs`:512×384 确定性地形(splitmix 随机游走河道 + 三倍频值噪声 + 泊松布点)。 - `SampleScript.fs`:自动演示脚本(靠近→提示→交互→关闭→离开全流程)。 - `src/LivingVillage.Headless/`:headless 入口 + PerformanceProbe(digest、基跑)。 - `scripts/make-jiangnan-art.py / make-cc0-art.py`:原创与 CC0 素材确定性生成。 - `third_party/`:Kenney Tiny Farm、Puny Characters(均 CC0 1.0,来源与 sha256 在 PROVENANCE.md)。 ## 模拟状态不变量(验收依据) 1. **数组快照**:`Sim.world.Npcs` 数组与 `InteractionResolver.resolve` / `refreshPrompt` 的 调用之间必须保持恒定——选择/提示路径从不 mutation(PrototypeTests 快照回归)。 2. **确定性 RNG**:世界推进只随 `World.Rng`(splitmix)与 `(seed)` 派生,禁止墙钟与 `System.Random`。昼夜/波纹/入场/动画帧全是 `tick → 值` 纯函数。 3. **存档 v1/v2**:`WorldSave` v2 头携带 `width height`,V1 读入回退默认。`worldDigest` 对版本头与边界 token 不敏感(剥头后重构 V1 常量语义再 SHA256),摘要只绑定模拟状态。 4. **地图 512×384 大世界**:`ProceduralMap.generate` 同 seed 两次生成逐字节一致; 村核矩形(24..38 × 16..34)由生成器不变式保证可通行。 ## 动画语义(P13,全部为 `tick → 帧` 纯函数) - 角色图集 `Assets/jiangnan-characters.png`:3840×48,每个 (变体,方向) 占 **6** 格 (4 走路 + 2 待机呼吸),格宽 32 高 48。 - `cellIndex = (variantIndex * 4 + directionIndex) * 6 + frameIndex`; 方向序 North/South/West/East = 0/1/2/3,变体序 Indigo/Ochre/Jade/Grey/StrawHat = 0..4。 - frameIndex:Walk 0..3 = 0..3,IdleOne/IdleTwo = 4/5。 - `animationFrame = (tick / 4) % 4`(16 tick 一循环);`idleFrame = (tick / 24) % 2`(48 tick 一循环)。 - `characterFrame isMoving tick`:移动取走路帧,静止取呼吸帧。Avatar 用 `Game.fs` 的 `avatarMoving`(比较前后位置);NPC 用 `npcSpriteSpecAtTarget`(目标向量 ≥0.5px 视为移动)。 - 世界图集 `Assets/jiangnan-world.png`:800×32,25 格。水面 3 帧在 slot 1/13/21, 炊烟 3 帧在 slot 22/23/24。 - `waterFrameTickAt tick x y = (tick / 32 + x + 2y) % 3` → Water / WaterSpriteB / WaterSpriteC。 相位按**世界 tile 坐标**偏移,所以同一帧里相邻河面砖显示不同相位;`waterFrameTick` 仍保留为 全局相位(旧断言)。 - `smokeFrameTick = (tick / 40) % 3` → SmokeSpriteA/B/C。炊烟在 `drawWorld` 的**末尾单独一遍** 绘制,位置 `offsetTile (house door) 0 -3`(屋脊正上方),保证画在屋瓦、竹子等所有 prop 之上。 - 以上帧选择只用 `tick` 与「是否移动」,无墙钟/`System.Random`;同 tick 必得同帧。 ## 预乘 alpha 与夜间光晕/炊烟接线(P14) - **机理**:MonoGame `SpriteBatch` 默认 `BlendState.AlphaBlend` 按**预乘**语义合成 (source blend = `One`,destination = `InverseSourceAlpha`)。因此直传未预乘的 `Color(r,g,b,a)` 会把 rgb 按原值直接加进帧缓冲:多层暖色叠加在核心区饱和成 **不透明纯白方块**(P13 夜晚灯笼的 75×75 白块即由此而来;`Color(255,0,0,60)` 实测渲染为 `(255,63,68)`)。光晕、色调等任何 alpha<255 的前景绘制都必须**按自身 alpha 预乘 rgb**。 - **正确写法**:`VillageArt.premultiply r g b a`(每通道 `round(c * a / 255)`,四舍五入; alpha=255 为恒等)。预乘后单通道 ≤ alpha,alpha<250 时不可能出现纯白。 - **夜间光晕**:`VillageArt.lanternGlowLayers cx cy radius rings r g b peakAlpha` 用同心圆 pixel run 近似径向衰减,每层色值预乘、核心多层叠加≈`peakAlpha`、边缘单层最暗; `Game.fs` 只在 `renderPlan.Props` 的 `RiverLantern` 实体处绘制,无灯笼处不产生光斑。 - **炊烟**:见上「动画语义」——烟雾是独立的后置绘制遍,与灯笼光晕同理。 - **角色站位**:`characterDestination` 把精灵下移 `characterFeetOffset = tilePixels/2`(16px), 使脚底贴合地砖底线;`drawGroundShadow` 用 `ellipseRuns` 画预乘半透明椭圆接触阴影。 - **回归**:Desktop 单测 `PremultiplyScalesEachChannelByItsOwnAlpha`、 `LanternGlowLayersArePremultipliedAndRadial`、`WaterRipplePhaseIsDistributedPerTile`、 `CharacterFeetSitOnTheTileGroundLine`;图像级 `scripts/check-no-white-blocks.py` 对夜晚录帧断言不存在 ≥64×64、每通道 ≥250、alpha ≥250 的方块(P13 旧帧 42/69 命中,修复后 0 命中)。 ## 性能基线命令 ```bash dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll --performance-baseline ``` - 口径:seed=42,npc=4,warmup=120,measure=6000,repetitions=3(见首行 `performance_config`)。 - 门槛:`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C` 三次一致 + `performance_determinism=PASS`。 - digest 变化处理:先比对 `WorldSave.save world` 全文文本(版本头变化不构成失败即 digest 规范化后等价); 再查 Sim.step 数值路径是否被改动(不允许)。历史归属见 `artifacts/perf-independent/*/README.md`。 ## M3 批验:流式可观测性、门槛与长局成本(M3-1) - **流式输出**:`--batch K D` 每个世界一完成就立即打印 `world=... OK elapsed_s=<秒>` 与 `[done k/N] elapsed=…s world=…` 并 flush,结束时 `batch_summary` 旧字段原样保留并追加 `workers=` 与 `wall_s=`(总墙钟)。完成序可能非升序,监督器按索引集合恰为 0..K-1 校验。 - **正式门槛缩尺 50×100 → 50×20**(`scripts/m3_supervise.py` 的 `PHASES.phase2`): phase2(50×100)实测 11.4 小时仅完成 6/50 世界,单世界 100 天约 6.8 CPU 小时, 全量约 335 CPU 小时,不可行(诊断见 `artifacts/m3-acceptance/final-candidate-20260921T095500Z/logs/phase2_20260921T101500.log.diagnosis.txt`)。 Hermes 实测单 worker 成本:1 天 25.6s / 5 天 123s / 10 天 254s / 20 天 ≈900s / 100 天 ≈6.8 CPU-h。缩尺只改规模,判据语义 check a/b/c 未动。 - **成本探针**:`--cost-probe [D1,D2,...]`(默认 `1,5,10,20,50,100`)逐段真实测量单世界 `ticks_per_s`、墙钟、`allocated_bytes` 与 gen0/1/2,不估算、不写死。 本机(单 worker,npcs=30,seed=42)实测: | days | elapsed_s | ticks_per_s | allocated_bytes | gen0 | gen1 | gen2 | |---|---|---|---|---|---|---| | 1 | 21.615 | 239837.5 | 19,001,478,648 | 2272 | 286 | 11 | | 5 | 111.645 | 232165.0 | 98,066,613,520 | 11728 | 11 | 2 | | 10 | 226.226 | 229151.8 | 202,369,225,952 | 24203 | 22 | 2 | 即约 **20.2 GB / 天·世界** 的分配;10 天内吞吐近乎平稳,超线性主要体现在更老的世界上 (Hermes 20 天 ≈45 s/天、100 天 ≈245 s/天)。 - **成本随年龄上涨的机理(只测不改)**:`World.Rumors: RumorEvent list` 永不裁剪, 每次谣言扩散 `rumor :: world.Rumors` 前插。实测 `rumor_trace count`:1 天 2025、 5 天 9821、10 天 19493(≈1950/天线性累积)。而 `latestRumorFor` / `rumorIsDuplicate` / `rumorPath` 每次都 `world.Rumors |> List.filter/tryFind` 全表扫描,因此每次聊天/对话的 成本随世界年龄线性增长、整轮成本随时长超线性(≈平方)增长;同时每次扫描与前插都产生 列表分配,推高 GC。`Mind.Memory` 有 `memoryCapacity` 上限(实测恒 64),不是主因。 - **修复方向候选(本单不实施,避免动确定性语义)**:①按 `rumorFreshnessTicks`/半衰期 确定性裁剪工作集(谱系/annal 另存);②以 `Map` 或逐接收者索引替代 全表扫描;③把 `latestRumorFor`/`rumorIsDuplicate` 限制到最近窗口。以上都需单开一单、 重跑 performance-baseline 与全部 digest 后由 Hermes 走冻结流程。 ## 已验证命令 ```bash # 完整重建(0 警告 0 错误) dotnet clean LivingVillage.sln 2>&1 | tail -n1 rm -rf src/*/bin src/*/obj dotnet restore LivingVillage.sln dotnet build LivingVillage.sln -c Release 2>&1 | tail -n3 # 全量测试 dotnet test src/LivingVillage.Desktop.Tests -c Release --no-build dotnet test src/LivingVillage.Kernel.Tests -c Release --no-build # M3 批次(流式;判据行与原格式逐字节兼容,仅行尾追加 elapsed_s) LV_BATCH_WORKERS=4 dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll --batch 4 10 # 长局成本阶梯(只读,真实测量) dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll --cost-probe 1,5,10 # 素材再生成(确定性,输出 byte-identical) python3 scripts/make-jiangnan-art.py && python3 scripts/make-cc0-art.py # Xvfb 录屏取证(沿用既有 pN harness 外参即可;harness 不入库) pkill -f "Xvfb :99"; Xvfb :99 -screen 0 1280x720x24 & sleep 2 ffmpeg -y -f x11grab -video_size 1280x720 -framerate 10 -i :99 -pix_fmt yuv420p out.mp4 LV_AUTOPLAY_SAMPLE=1 LV_AUTOPLAY_DAYLIGHT=1 LV_AUTOPLAY_RECORD=1 \ dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll # P13 动画证据(确定性接触表 + 真实运行帧) python3 scripts/make-animation-evidence.py # -> evidence/walk-idle-frames.png 等 # 真实运行帧(白天 / 夜晚各一次;record 目录与间隔可用环境变量覆盖) timeout 45 xvfb-run -a -s "-screen 0 1280x720x24" env \ LV_AUTOPLAY_SAMPLE=1 LV_AUTOPLAY_DAYLIGHT=1 LV_AUTOPLAY_RECORD=1 \ LV_RECORD_DIR="$PWD/evidence/day-run" LV_RECORD_EVERY=2 \ dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll timeout 45 xvfb-run -a -s "-screen 0 1280x720x24" env \ LV_AUTOPLAY_SAMPLE=1 LV_AUTOPLAY_RECORD=1 \ LV_RECORD_DIR="$PWD/evidence/night-run" LV_RECORD_EVERY=2 \ dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll ``` - `LV_RECORD_DIR` 默认 `/tmp/lv-p5-prompt`,`LV_RECORD_EVERY` 默认 25(每 N 次绘制存一帧)。 跑完 `sample result=ok`;连续移动帧(如 day-run 的 0136..0146)即走路逐帧采样。 - 夜晚白块图像级回归(0 命中 = 通过;对 P13 旧帧应报 42/69 命中): ```bash python3 scripts/check-no-white-blocks.py evidence/night-run ``` ## 取证快照位置(近期) - `/tmp/lv-p2-ui/`、`/tmp/lv-p3-fixes/`(标题/键位/P5 提示与修) - `/tmp/lv-p6-art/`(昼夜光照/四帧行走/标题入场) - `/tmp/lv-p7-map/`(512×384 大世界巡游帧 + 夜灯对照) - `/tmp/lv-p8-cc0/`(CC0 同 seed 对照帧) - `/tmp/opencode/lv-p13/evidence/`(P13:`walk-idle-frames.png`、`water-3phase.png`、 `smoke-3phase.png`、`water-2frame.png`、`smoke-2frame.png`、`day-panorama.png`、 `night-lantern.png`、`day-night-compare.png`、`walk-real-run.png`、`day-run/`、`night-run/`) - `/tmp/opencode/lv-p13-evidence/`(P14 修复后重录:`day-panorama.png`、`night-lantern.png`、 `day-night-compare.png`、`chimney-closeup.png`、`night-lantern-closeup.png`、 `white-block-check.txt`、`day-run/`、`night-run/`;同步一份到 `evidence/`) - `artifacts/perf-independent//`(digest 3 联测回读)