1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
|
# 维护说明
结论先行:主分支保持 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<RumorId, RumorEvent>` 或逐接收者索引替代
全表扫描;③把 `latestRumorFor`/`rumorIsDuplicate` 限制到最近窗口。以上都需单开一单、
重跑 performance-baseline 与全部 digest 后由 Hermes 走冻结流程。
## M6a 批验:谣言工作集容量上界与按天淘汰
- 实施 M3 的三条修复方向:
①`RumorEvent` 新增派生字段 `DayIndex = Tick / ticksPerDay`,不参与存档序列化,读档由 `Tick` 纯函数
重建,故 v1/v2/v3 存档文本逐字节不变;
②新增可变参数 `rumorCapacity`(默认 16384)与 `rumorRetentionDays`(默认 3,>= 新鲜窗口 3 天),
纯函数 `trimRumors` 只保留最新 N 条并丢弃早于 `nowDay - rumorRetentionDays` 的条目;容量内且新鲜时
零拷贝原样返回;两个前插点 `chooseDialogue`/`chat` 统一走 `consRumor`;
③列表最新在前,`latestRumorFor`/`rumorIsDuplicate` 改早停递归(越窗即停、无中间列表),
`nextRumorId` 改读表头 O(1)。
- 等价性:保留窗口(>= 3 天)覆盖两个查询的新鲜判定,窗口外条目本就会因 `rumorStrengthAt <
rumorMinimumStrength` 被过滤,故裁剪不改变任何查询结果;「表头为最大 Id」不变量在裁剪后保持。
- 实测(本机单 worker,npcs=30,seed=42,`--cost-probe`;同机 M3 未裁剪基线见上节):
| days | 未裁剪 elapsed_s | M6a elapsed_s | 未裁剪 allocated_bytes | M6a allocated_bytes |
|---|---|---|---|---|
| 1 | 23.4(×3) | 23.4(×3) | 19,001,341,888 | 18,898,357,944 |
| 10 | 240.6 / 268.6 | 236.2 / 274.0 | 202,373,638,848 | 191,916,294,064 |
| 20 | ≈900(M3 记录) | 503.1 | — | 384,468,393,040 |
10 天分配/gen0 稳定下降约 5%;超线性项改善更大:M3 记录 20 天 ≈45 s/天,M6a 后 20 天 ≈25 s/天
(≈1.8×),单日成本回归平稳。
- 工作集实测(`evidence/` 探针,seed=42,npcs=30):第 5 天 `Rumors` 7796(收敛)vs 未裁剪 9821,
两者表头 `Id` 同为 9820,证明创建序列不变、仅淘汰已不可用的旧条目。
- 门槛:`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C` 三次一致,
`performance_determinism=PASS`;Kernel 89 / Desktop 94 全绿。
## P16 批验:启动画面 / 开始界面 / HUD 精美化
- ①启动画面:新增纯函数模块 `LaunchScreen.fs`(`durationFrames=150`、`fadeInFrames=45`、
`title/subtitle/skipHint`、`revealProgress/veilAlpha/isComplete/skipRequested`);`Game.DrawSplash`
以真实游戏内渲染绘制标题/副标题,Esc 或空格可跳过,`splashFrame` 单调推进。
- ②开始界面:`MenuState.items` 收敛为三项——无存档 `[NewGameItem; LoadItem; ExitItem]`、有存档
`[ContinueItem; LoadItem; ExitItem]`;标签 开始/继续/读档/退出;设置与操作说明不再占位(游戏内
F1/F2/F3 与 P 仍可开)。
- ③HUD:原 `DrawM5Overlay` 拆为 `DrawHudBar`/`DrawTransientStatus`/`DrawPanel`;顶部窄信息条承载
时段/时间/倍速/体力/钱币,无大黑块;`HudLayout.fs` 固化 `barHeight=36`/`accentHeight=3`/
`paddingX=16`/`segmentGap=20`/`textScale=2` 与昼/夜调色板(冬夜冷色)。`FloatingPrompt` 顶避让改为
全宽 `hudTopBarHeight=39`,移除 `hudTopRightWidth/Height`。
- ④文本:全部新 UI 文案走 CJK 图集;图集 228→238 字(补 `币 段 跳 过 空 格 清 晨 黄 昏`),行数仍 15、
PNG 仍 384×360;`Assets/cjk-glyphs.txt`、`CjkGlyphAtlas.characterTable`、`requiredUiLabels` 三者一致,无缺字。
- ⑤回归:新增 `P16UiTests.fs`(启动跳过/菜单导航/HUD 布局确定性);Desktop 94→104,Kernel 89 不变
(未触碰 Kernel/Sim.fs 数值路径)。
- ⑥证据:`/tmp/opencode/lv-p16-art/`(`frame-day-0{1,2,3}.png`、`frame-night-0{1,2,3}.png`、
`frame-splash-01.png`、`hud-bar-day.png`、`hud-bar-night.png`、`sample.mp4` 13.1 s、
`final_verification.txt`);`check-no-white-blocks.py` 对 92 帧 0 命中。
- ⑦(P16-FIX 证据补拍)`frame-menu-0{1,2}.png` 实为游戏内帧,不是开始界面;新增确定性截图模式
`LV_AUTOPLAY_MENU_SHOT=1`:跳过 hero 自动开始,停在开始界面按固定帧表取样。帧表抽为纯数据
`MenuShotScript`(splash 淡入中 30 / 初始高亮 130 / 向下 140 / 高亮读档 150 / 退出 170),附 1 例
回归;`Game.CaptureMenuShot` 复用 `SaveBackBuffer` 落图。产物:`menu-real-initial.png`(初始高亮
「开始」)、`menu-real-load.png`(按向下后高亮「读档」)、`frame-splash-mid-01.png`(淡入中帧)。
像素复验:初始帧仅第 1 行含高亮色 `(245,222,120)`(115 px),load 帧仅第 2 行含(111 px),
两帧差异 575 px,确证选择项迁移。
- 门槛:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
一致、`performance_determinism=PASS`。
## P17 批验:场景细节 / 人物动效 / 昼夜光照精修
- ①街边中式细节物:`scripts/make-jiangnan-art.py` 世界图集由 25 格扩到 28 格(新增 slot 25 石灯笼 /
26 水缸 / 27 摊贩摊位,全部烘焙进 `Assets/jiangnan-world.png`,尺寸 800x32→896x32),游戏侧只做
`tileSourceRectangle` 贴片、无程序几何。`VillageArt` 新增 `RenderPropKind.StoneLantern/WaterVat/
MarketStall` 与 `worldTileCount=28`;`sampleRenderPlan` 沿石路摆 4 件(石灯笼 4,14 / 16,14,水缸
12,11,摊位 3,11)。图集脚本二次运行 byte-identical(sha256 一致)。
- ②村民走路节奏差异化:`VillageArt` 新增 `NpcTrade`(Farmer/Peddler/Scholar,按 `NpcId mod 3` 纯函数
分配)与 `WalkRhythm`(走路/待机窗口 + 相位)。农夫 4/24、货郎 3/20(更快)、书生 6/36(更慢);
`npcSpriteSpec` 改走各自节奏,`animationFrame/idleFrame/characterFrame` 保持农夫节奏即旧语义。
- ③昼夜光照精修:`M6Presentation.lightingBlend` 过渡窗口参数化(清晨 4:00→6:30、黄昏 19:30→22:00)
并把小时改分数(原为整数小时,过渡只有 3 档),新增纯函数 `transitionWarmth`(晨昏中点火暖色峰值
1,正午/午夜 0)驱动 `backgroundAtWarmth/worldTintAtWarmth`,`M6RenderProfile` 增 `SkyWarmth`。
- ④回归:更新/新增确定性测试(行走节奏、光照锚点与暖色、图集 28 格与 PNG 尺寸、证据起时纯函数);
Desktop 107 / Kernel 89 全绿(未触碰 Kernel/Sim.fs 数值路径)。
- ⑤证据:`/tmp/opencode/lv-p17-art/`(`p17-day-12h-0{1,2}.png`、`p17-dusk-20h45-0{1,2}.png`、
`p17-night-00h-0{1,2}.png`、`p17-diff-before-dawn.png` vs `p17-diff-after-dawn.png`(同一 6:00 tick,
P17 前/后各 1 帧)、`sample.mp4` 13.1 s);`check-no-white-blocks.py` 对所选 6 帧 0 命中。起时用新增
环境钩子 `LV_AUTOPLAY_START_HOUR`(纯函数 `M6Presentation.startTickForHour`,桌面演示专用,不进 Kernel)。
- ⑥(P17-FIX 起时钩子修复)旧 `p17-day-0{1,2}` / `p17-dusk-0{1,2}` 帧的钩子在 sample/autoplay 取证路径下
失效:`StartNewGame` 重新创建世界时按 `daylightAutoplay` 把 Tick 重设为 0/6:00,覆盖了
`createInitialWorld` 设好的值,导致黄昏/昼取证实际全为「夜晚 00:00」,P17 光照精修未被真实帧展示。
修复:抽纯函数 `M6Presentation.resolveStartTick (startHour option) (startAtDaylight)`(显式小时优先,
否则 daylight 6:00,否则 0),`createInitialWorld` 与 `StartNewGame` 统一经 `evidenceStartTick`/`withStartTick`
取同一 startTick(占位/任务日一致),`new-game=ok` 日志追加 `tick=` 便于机器核验;非取证人工路径不受影响。
回归:`StartTickResolutionHonoursEvidenceHourThenDaylightThenMidnight`(Desktop 107→108),
实测 `tick`:昼 12.0→2,592,000、黄昏 20.75→4,482,000、夜 0→0,`sample.mp4` 为黄昏 20:45。
以上新文件名(`p17-day-12h-*` / `p17-dusk-20h45-*` / `p17-night-00h-*`)为准,旧名作废。
(P18 复核确认:这批帧起时虽已正确为 20:45,但画面仍为夜蓝冷调,原因是暖色渲染链缺失,
见 §P18;P17 黄昏/夜间帧的起时有效、但暖色未生效,最终以 P18 证据为准。)
- 门槛:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
一致、`performance_determinism=PASS`。
## P18 批验:黄昏暖色调真实渲染整改
- 根因(渲染调用链):①`lightingBlend` 黄昏暗化窗口 19:30–22:00,使 20:45 的 `blend=0.5`,`Mode` 直接落到
Night;②暖色仅经 `worldTintAtWarmth` 的**乘性** tint(R+10 / G−6 / B−26)施加于世界,而乘性通道无法把
低 R 的绿色植被染成橙色,地面依旧偏绿;③`timePeriodLabel` 20:00 起即为「夜晚」,与暖色窗口错位。
- 修复:黄昏**暗化**窗口改为 20:00→22:00(20:45 `blend=0.375`,仍 Day),暖色 bump 独立为 19:15–21:45
(峰值 20:30);`backgroundAtWarmth` 暖移加强(R+120 / G+20 / B−90),`worldTintAtWarmth`(R+70 / G−10 /
B−75);新增纯函数 `goldenVeilAlpha`(0..150,峰 150),`Game.DrawWorldView` 在世界与角色绘制后叠加
**预乘暖色整屏 veil**(`premultiply 255 150 70 veilAlpha`,预乘按自身 alpha 缩放、不会饱和成白块),
补上「加性暖光」这一缺失环节;`LanternGlow = max blend (warmth * 0.8)` 让黄昏灯笼同步暖亮;
`timePeriodLabel` 黄昏延到 21:00。
- 回归:更新光照锚点/单调/确定性用例并新增 veil 锚点(正午/午夜 0、20:30 峰 150、19:00 前 0),
Desktop 108 / Kernel 89(Kernel 未动)。
- 证据:`/tmp/opencode/lv-p18-art/`(`p18-dusk-20h45-0{1,2}.png`、`p18-night-deep-01.png`、
`p18-before-old-dusk-blue.png` vs `p18-after-new-dusk-warm.png`、`sample-dusk.mp4` 13.2 s)。
像素复验:旧黄昏画面均值 (55,81,54)(R−B≈1,冷蓝)→ 新黄昏 (153,115,57)(R−B≈95,暖橙),深夜
(34,60,56) 仍冷;`check-no-white-blocks.py` 0 命中。
- 门槛:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
一致、`performance_determinism=PASS`。
## P19 批验:白天偏黄过曝治理(daylight 色调路径)
- line-level 定位:offender 唯一是 `M6Presentation.worldTintAtWarmth`(`M6Presentation.fs:81-87`)的 **day 基线**
—— `lerp 248 136 blend`(:84)与 `lerp 224 184 blend`(:85)在 `blend=0`(正午)仍给出 `(255,248,224)`:蓝通道
被压约 12%,整场景乘性偏黄。黄昏暖色链在白天并不生效,已逐一排除:`transitionWarmth`(:51-60)bump 仅在
4.0–6.5 与 19.25–21.75;`goldenVeilAlpha`(:65)正午 = 0;`LanternGlow = max blend (warmth*0.8)`(:101)
正午 = 0;`lightingBlend`(:36-43)正午 = 0。故「白天偏黄」= day world-tint 基线,与黄昏暖窗无关。
- 修复:day 基线改中性白 `(255,255,255)`,乘性恒等(geometry 保留 true white balance,无伪色彩);night 基线
`(118,136,184)` 与 `warmth` 暖移不变,黄昏/清晨暖橙完全保留。
- 回归:新增 `NoonLightingIsWarmthFreeWithNeutralWhiteBalance`(DesktopTests);PrototypeTests 增白天
10:00–15:00 平台断言(warmth=0、通道中性、veil=0、glow=0);Desktop 108→109,Kernel 89 不变。
- 证据:`/tmp/opencode/lv-p19-art/`(`p19-noon-12h-0{1,2}.png`、`p19-dusk-20h45-01.png`、
`p19-night-00h30-01.png`、`p19-before-noon-yellow.png` vs `p19-after-noon-neutral.png`)。画面均值:
正午前 `(71,109,66)` R−B=+5.1(偏黄)→ 正午后 `(70,109,75)` R−B=−5.3(中性);黄昏 `(146,113,61)`
R−B=+85(暖橙保留);深夜 `(34,60,56)` 冷调;白块回归 0 命中。
- 门槛:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
一致、`performance_determinism=PASS`。
## P20 第一步:参数化确定性生成器与视口裁剪(大地图扩程前置)
- 新增纯函数模块 `src/LivingVillage.Desktop/MapGen.fs`:`generate : Params -> Result`,尺寸参数化
(默认仍是 64x48;可用 `generateWithSize 256 192 seed`)。组成:
splitmix64 随机游走河道(`RiverCount`/`RiverWidth`)、2-3 octave value noise 地形(草地/泥炭)、
泊松式最小间距布点(石头装饰);核心活动区先整平为草地,再由 **BFS(4 邻接、非水可走)**
从核心中心洪泛校验 5x6=30 个 spawn 全部可达,不可达则沿直线确定性修整并复验,结果写入 `ReachabilityOk`。
纯函数、无时钟/随机源:同 seed 两次生成逐字节一致(`Array.mapi` 校验 + checksum)。
- 视口裁剪纯函数 `MapGen.visibleTileRange`/`visibleTileCount`(与既有 `VillageArt.drawWorld` 的
`tx0..tx1/ty0..ty1` 窗口一致),`drawWorld` 已改为调用它,行为不变;`drawMapFitted`/`drawMapViewport`
为证据专用绘制(整图等比 + 1:1 裁剪),不进正常游戏循环。
- 存档兼容:沿用既有 v2/v3 的 `mapWidthTiles`/`mapHeightTiles` 边界字段(读旧档仍按 64x48),
P20 只补参数化边界测试,未改 Sim 数值语义;`final_digest` 与基线一致。
- 回归:`MapGenTests`(同 seed byte-identical、异 seed 不同、256x192 30 spawn 全可达、默认尺寸 64x48、
裁剪范围/端点钳制)6 条;Kernel `SaveRoundTripsParameterizedMapBounds`(256x192 v2 round-trip)1 条。
Desktop **109 -> 115**,Kernel **89 -> 90**。
- 实测性能(Xvfb,本机):生成 256x192 = **52 ms**、64x48 = **1.9 ms**;256x192 生成
`reachable_ok=true`、`reachable_tiles≈47684`;裁剪后单帧仅绘制 **1050/49152 = 2.1%**
(`culled=97.9%`),离屏单帧 **0.62 ms/帧**(implied ≈1610 fps,仅 harness 立即模式)。
- 证据:`/tmp/opencode/lv-p20-art/`(`mapgen-256x192-seed{4242,7,99}.png` 整图三 seed、
`mapgen-culled-256x192-seed{...}.png` 裁剪帧、harness 计数日志)。白块回归 0 命中。
- 门槛:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
3 次一致、`performance_determinism=PASS`。
## P20 第二步:河道连续性不变量 + 桥 / 石板路网 / 沿河民居
- 河道重做:由「`not (inCore)` 逐格随机游走」改为**贯穿全图的东西向连续水带**(`Rivers : River list`,
每条每列恰好一段、宽度恒 `>= max 3 RiverWidth`),置于核心区上下两侧且与核心不相交。
新增不变量测试 `RiversAreContinuousWithoutGapsOrIsolatedTiles`:逐列水段数 == `Rivers.Length` 且每段
`>= RiverWidth`、任意水格 4 邻接水邻居 `>= 2`(禁止断裂缺口与孤立水点)。
- 交通结构(`Result` 新增 `Bridges`/`Paths`/`Buildings`/`BridgeCrossingsOk`):
每条河 2 座桥(2 格宽,桥面仍记水但 `isWalkable` 放行);主石板路沿核心中心行横贯全图,
每座桥有垂直连接路贯通两岸与远岸路;远岸路两侧布置民居矩形(`Building`:白墙黑瓦两行色块,门朝路)。
`MapGen.isWalkable`/`MapGen.floodFill` 公开供测试:`ReachabilityOk` 现在同时要求 30 spawn、所有门、
所有桥面可达;`BridgeCrossingsOk` 要求每座桥两岸均被同一连通域覆盖。
- 回归新增 4 条:河道连续性、桥/路/民居存在且沿河、桥两岸可达(spawn/门/桥全连通)、结构逐字节确定性。
Desktop **115 -> 119**,Kernel **90** 不变。
- 渲染证据(`drawMapFitted` 每瓦片 6px 放大 + `drawMapViewport` 1:1 + 桥中心 detail 帧);
`p20b-map-*`(整图,含桥/路/白墙黑瓦民居)、`p20b-detail-*`(1:1 桥/路/民居近景)、
`p20b-culled-*`。白块回归 0 命中;整图 1536x1152。
- 实测(Xvfb,本机):256x192 生成 ≈ **63 ms**、64x48 ≈ **2.0 ms**;`reachable_ok=true`、
`reachable_tiles=47652`(= 49152 - 两河水面 1524 + 桥面 24,三 seed 恒定,印证连续性不变量);
裁剪帧 1050/49152 = 2.1%,1:1 单帧 ≈ 0.7 ms(implied ≈1300-1600 fps)。
- 门槛同上:clean rebuild 0/0;`final_digest` 3 次一致、`performance_determinism=PASS`。
## P20 第三步:河景与可玩接入收口
- 默认可玩世界改走 **256x192 生成器**:`Game` 启动时 `Sim.configureBounds 256 192` 并
`MapGen.generateWithSize 256 192 42UL`,用 `WorldBootstrap.initialWorldInMap` 以生成器核心可达区站位
(30 NPC 全落可达核心,avatar 落核心中心);`LV_LEGACY_MAP=1` 或 `LV_MAP_SCALE>1`/`LV_MAP_TOUR=1`
回退旧路径(64x48 样例世界 / ProceduralMap 大世界)。`LV_AUTOPLAY_SAMPLE=1`(非巡游)也保持 64x48
以确保既有录像可复现,只有显式 `LV_RIVERSCAPE_TOUR=1` 才启用生成器世界;启动日志打印 `world-mode=`。`WorldBootstrap.initialWorldWithPlacement`
为显式站位入口,旧 64x48 函数字节口径不变(Kernel/Desktop 既有断言不动)。
- 渲染接入:`VillageArt.drawWorld` 新增 `pixel` 与 `mapGen : MapGen.Result option`——
Some 时按生成器瓦片绘制地面(1 水 / 2 石板路 / 其余草地)、叠加 `drawMapStructures`(桥面 + 白墙黑瓦民居 + 门)
与桥头 `LanternSprite`,并跳过 64x48 样例平面/道具;水格沿用既有 `waterFrameTickAt` 多相位波纹。
- 河景灯笼:新增 `VillageArt.bridgeLanternTiles`(每桥每列最上/最下格 = 桥两端);夜间 `LanternGlow`
沿用既有预乘同心圆 `lanternGlowLayers ... 255 214 150`(软暖盘,无过曝白芯),生成器世界用桥头灯笼位置。
- 确定性:`MapGen.serialize` 对尺寸/seed/瓦片/河道/桥/路/民居做序列化,同 seed 逐字节一致;
`SameSeedSerializationIsByteIdentical` + `MapWorldPlacementUsesReachableCoreSpawns` 两条新测试。
- 回归:Desktop **119 -> 121**,Kernel **90** 不变;clean rebuild 0 警告 0 错误;
`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C` 3 次一致、
`performance_determinism=PASS`(未触 Kernel/Sim 数值路径)。
- 证据(`LV_AUTOPLAY_SAMPLE=1 LV_RIVERSCAPE_TOUR=1`,`LV_RECORD_NAME=p20c-*`):真人走位巡游沿桥/路/民居门
逐帧录制,`/tmp/opencode/lv-p20-art/p20c-day-*.png`(`LV_AUTOPLAY_START_HOUR=12.0`)与
`p20c-night-*.png`(20.75)各若干;挑出 `p20c-day-bridge-01.png`、`p20c-day-buildings-01.png`、
`p20c-night-bridge-01.png`、`p20c-night-lantern-01.png` 等代表性帧;716 张全过白块回归 0 命中。
## 大世界(P21 性能与镜头收口)
- 默认世界:`Game` 启动即 `Sim.configureBounds 256 192` + `MapGen.generateWithSize 256 192 42UL`
(`MapGen.defaultParams`:`RiverCount=2`、`RiverWidth>=3`、`CoreSide=14`、5x6=30 spawn、
splitmix64 河道 + 2-3 octave value noise + 泊松布点)。核心区先整平,再 BFS 校验 30 spawn
与桥/路/民居门全部连通(`ReachabilityOk`/`BridgeCrossingsOk`);站位由
`WorldBootstrap.initialWorldInMap` 取核心可达域,avatar 落核心中心。
- `world-mode` 回退开关(启动日志会打印实际模式与 bounds):
- `LV_LEGACY_MAP=1` -> 旧 64x48 样例世界;
- `LV_MAP_SCALE=N`(N>1)-> ProceduralMap 大世界 `64N x 48N`;
- `LV_MAP_TOUR=1` -> ProceduralMap 巡游;
- `LV_AUTOPLAY_SAMPLE=1`(非巡游)-> 保持 64x48,保证既有录像可复现;
- `LV_RIVERSCAPE_TOUR=1` -> 显式启用 256x192 生成器世界巡游。
- 视口裁剪:`MapGen.visibleTileRange` 只绘制可见瓦片(`drawWorld` 已接入),256x192 下
1280x720 视口约绘制 `1050/49152 = 2.1%`,`culled=97.9%`(P20 第二步离屏实测)。
- 实测性能(本机 Xvfb 1280x720x24,Desktop Release,256x192 可玩世界,`LV_UNLOCK_FPS=1`
放开 vsync 以测真实上限):
- 帧率:`LV_RIVERSCAPE_SECONDS=60` 实测 58 个有效秒采样(丢 5 秒 warmup),
**mean 230.4 FPS / min 199.5 FPS / max 302.8 FPS**;默认 vsync+固定步长为稳定 60 FPS。
- 内存:运行中 `VmRSS` 181.9–183.4 MB(≈182.7 MB 均值,含 MonoGame 运行时与图集),
60s 内平稳无泄漏。
- 生成:256x192 约 52–70 ms;64x48 约 2 ms(P20 实测)。
- headless:`--performance-baseline` 3 次 `ticks/s` 区间 **189,595–445,377**(宿主负载相关),
`allocated_bytes≈3.03 MB/6000 ticks`、`gen0=0`,`final_digest=953775FA…`、
`performance_determinism=PASS`(未触 Kernel/Sim 数值路径)。
- 取证:`/tmp/opencode/lv-p21-art/`(P22 重挑后的游戏内代表帧 9 张:
`p21-day-bridge-01`/`p21-day-river-01`/`p21-day-buildings-0{1,2}`、
`p21-dusk-bridge-01`/`p21-dusk-street-01`、`p21-night-bridge-01`/`p21-night-street-01`/
`p21-night-lantern-01`;原始帧 `raw-day/`、`raw-night/`、`raw-night2/`),白块回归 0 命中。
## 大世界默认开启评估(P22)
- 结论:**256x192 生成器世界已经是默认可玩世界**(P20 第三步即已切换,`riverscapeActive`
在非 legacy/非 map-scale/非 sample 时成立),`LV_LEGACY_MAP=1` 等回退开关保留。P22 复核
证据支持维持现状,未再改默认值。
- 实测对比(本机 Xvfb 1280x720x24,Desktop Release,`LV_UNLOCK_FPS=1` 放开 vsync,
丢 5s warmup 后按秒采样;两者工作负载不同,仅作量级对比):
- 256x192 生成器(巡游,1x 模拟):`mean 230.4 / min 199.5 / max 302.8 FPS`(58 样本)。
- 64x48 样例世界(sample 脚本,2x 模拟):`mean 163.5 / min 113.9 / max 224.3 FPS`(65 样本)。
小世界无裁剪收益、屏内 NPC 更多且 2x 模拟,反而更慢;两种世界都远高于 60 FPS 目标。
- 内存:256x192 运行中 `VmRSS` 181.9–183.4 MB;64x48 为 179.5–182.3 MB —— 差异在噪声内,
内存由 MonoGame 运行时/图集主导,与地图尺寸基本无关。
- 生成耗时:256x192 约 52–70 ms、64x48 约 2 ms(一次性,发生在进入世界前,无可见卡帧)。
- 风险:唯一成本是启动时约 60 ms 一次性生成,无逐帧开销(视口裁剪 97.9%);headless
`final_digest` 与 `performance_determinism` 不受影响(未触 Kernel/Sim 数值路径)。回退路径
完整,可随时 `LV_LEGACY_MAP=1` 或 `LV_MAP_SCALE=N`。**评估结论:维持 256x192 默认。**
- 遗留观察(仅记录,留给后续单):
- 初始 NPC 站位是核心区内的**规则网格**(`MapGen.fs` 第 253 行「30 个 spawn:核心区内的
规则网格」),P20 巡游首帧会看到整齐排布;巡游开始后自然散开,暂不改。
- 个别帧右侧有**孤立砖石小块**(地面 `GroundTile.Stone=2` 单格,`VillageArt.fs:664` 绘
`StonePathSprite`),疑似路网/门阶的单格支路残留,需后续单用定点帧确认后再修。
## 大世界扩程 512x384(P23)
- Kernel/Sim:边界本就是可变参数(`Sim.configureBounds w h`,默认仍 64x48);512x384 直接可用。
存档 v2/v3 头两字段即 `bounds.w|bounds.h`,v1 旧档仍按 64x48 读入(`WorldSave.parse`)。
512x384 存档 round-trip、同 world 两次保存逐字节一致、长跑 20000 tick 的 avatar/NPC 边界
与有限性均由 Kernel 用例覆盖(Kernel 90 -> 92)。
- MapGen 尺寸自适应(`paramsForSize`):
- 河道数量约每 96 行一条:64x48/256x192 仍 2 条,512x384 得 4 条;第 3 条起在核心区上/下方
逐层外扩,互不重叠、不压核心,仍满足「每列恰好一段、宽度 >= RiverWidth、水格 >=2 水邻居」。
- 噪声特征尺度封顶 4x(`noiseWidthScale`),避免大图退化成过度平滑的放大图;泊松布点间距
`w/24`、尝试数 `(w*h)/16` 本就随尺寸缩放。
- **64x48 / 256x192 生成结果逐字节不变**(新增 checksum 锁定用例:64x48=6042052、
256x192=1284656962)。默认世界仍是 256x192,本轮不切换。
- 生成耗时(本机实测,单次):64x48 ≈ 3 ms;256x192 ≈ 55 ms;512x384 ≈ 134-248 ms
(一次性、进入世界前)。
- 512x384 巡游帧率(Xvfb 1280x720x24,Desktop Release,`LV_UNLOCK_FPS=1` 放开 vsync):
单趟巡游 161.7 s,`perf-summary samples=156 mean_fps=153.84 min_fps=51.64 max_fps=228.81`;
运行中 `VmRSS` 185.9-201.9 MB。默认 vsync+固定步长为稳定 60 FPS。
- 取证:`/tmp/opencode/lv-p23-art/`(`p23-map-512x384-seed{4242,7}.png` 整图真实渲染、
`p23-culled-*`/`p23-detail-*` 视口/近景、`tour512/` 巡游帧),白块回归 0 命中。
- 取证钩子:`LV_RIVERSCAPE_WIDTH`/`LV_RIVERSCAPE_HEIGHT`(默认 256x192)把生成器世界扩到
512x384 跑巡游/性能;`LV_MAPGEN_SHOT=1` + `LV_MAPGEN_WIDTH/HEIGHT/SEED` 出整图。
## 民居密度参数(P24)
- `MapGen.Params.ExtraRiversideHouses`:大图额外沿河民居数量,默认 0;`paramsForSize` 仅对
`width>256 || height>192` 设为 **10**。512x384 在原有 8 座桥点民居上再按现有路网泊松散布
10 座(`structureRng` 确定性),**共 18 座**,门全部接入路网且 BFS 可达;64x48/256x192 仍
4 座、checksum 6042052 / 1284656962 不变。
- 民居渲染改用 256x192 同款部件:黛瓦(`RoofRidgeSprite`/`RoofEaveSprite`)+ 木墙/窗
(`WallWoodSprite`/`WallWindowSprite`)+ 门(`DoorSprite`),替换原黑白矩形色块。
- 村民可见性证据钩子 `LV_NPC_VISIBLE_LOG=1`:每秒打印 `visible-npcs=K of 30`;512x384 巡游中
观测到最大 30/30 全部在屏(`/tmp/opencode/lv-p24-tour.txt`)。
- 取证:`/tmp/opencode/lv-p24-art/`(`p24-map-512x384-seed{4242,7}.png` 整图、
`p24-zoom-houses.png`/`p24-zoom-villagers.png` 局部放大含民居与村民、`p24-tour-0{1,2,3}.png`
tour 页、`tour512/` 全量帧),白块回归 0 命中。
## 巡游村民可见性 + 孤立石板治理(P25)
- **成因**:NPC 行动目标 `Sim.actionTarget` 固定在遗留四个活动点(厨房 10,10 / 家 52,10 /
集市 32,24 / 工位 52,38),512x384 世界里它们落在左上区域,NPC 出生后从核心走向那里;
原巡游路点走桥/门/核心,故大多数采样帧 `visible-npcs` 只有 0-3。
- **解法(Desktop 取证钩子,不动 Kernel/Sim 数值路径)**:新增 `LV_RIVERSCAPE_VILLAGE_TOUR=1`,
巡游路点改为在这四个村民活动点之间环游(`villageTourWaypoints`)。512x384 跑 150 s:
`visible-npcs>=8` 的采样 **112/152 = 74%**,进入村庄段后 **108/122 = 89%**(后 90 s 91%),
峰值 30/30;日志 `/tmp/opencode/lv-p25-village150.txt`。确定性不变。
- **孤立石板修复**:成因是步骤 4 的泊松装饰石块(`GroundTile.Stone=2` 单格渲染成灰石板),
512x384 有 292 个孤立石板。新增 `Params.DecorativeStones`(默认 true),`paramsForSize`
对大于 256x192 的图关闭 → 大图不再产孤立路面 tile(装饰关闭后大图石块全部属于路网)。
64x48/256x192 保持开启,checksum 6042052 / 1284656962 不变。
- 回归:`LargeMapHasNoIsolatedStoneTiles`(大图无孤立石块、石块全属路网)、
`DecorativeStonesDisabledOnLargeMaps`;Desktop 134 -> 136。
- 取证:`/tmp/opencode/lv-p25-art/`(`p25-map-512x384-seed4242.png` 整图无灰石板、
`p25-zoom-village.png` 放大含 >=8 村民、`p25-tour-0{1,2,3,4}.png` tour 页、`tour/` 全量帧),
白块回归 241 张 0 命中。
## 江南水乡第 4 档:512×384 美术可读性与质感(P26)
- **① 民居立面第二变体**:世界图集由 28 格扩到 **32 格**(尺寸 896x32→1024x32),新增
slot 28 屋檐挑出 / 29 木格栅窗 / 30 院门;`scripts/make-jiangnan-art.py` 仍为确定性管线,
二次运行 byte-identical。`VillageArt.houseFacadeVariant (building) = (Left+Top) % 2` 为纯函数,
沿河沿路确定性交替:变体 A 保持 P24 的黛瓦脊 + 木墙/窗 + 门;变体 B 用 `RoofEaveOverhangSprite`
向两侧各挑出一格,墙身用 `WallLatticeSprite`/`CourtyardDoorSprite`。只改绘制分支,
门可达不变量与 `MapGen` 瓦片零改动(64x48=6042052、256x192=1284656962 不变)。
- **② 河岸细节(仅 512)**:`MapGen.Params.RiverDecorations`(默认 false;`paramsForSize` 仅对
`width>256 || height>192` 置 true)。`MapGen.Result.Decorations: RiverDecoration list` 三类——
护岸石垒 `RevetmentStone` / 芦苇 `Reeds` / 垂柳 `Willow`(占上下两格)。**纯视觉层,不写入任何
瓦片**,故沿用 `DecorativeStones` 参数化先例的同时不产孤立石板;装饰只落在紧邻水面的岸格,
并避开路网/桥面/民居/核心区。回归:`RiverDecorationsOnlyEnabledOnLargeMaps`、
`RiverbankDecorationsSitOnBanksAndNeverBlockPathsOrBridges`(岸格相邻水 + 不压路/桥/核心)、
`LargeMapRiverDecorationsAreDeterministic`。
- **③ 夜晚光照二次整改(仅 Desktop 渲染层)**:新增纯函数 `M6Presentation.moonlightVeilAlpha`
(0..72,仅 deep night 有值,暖色晨昏窗口被 `warmth` 压制)与 `moonlightRgb = (120,170,235)`;
`Game.DrawWorldView` 在金辉 veil 之后叠加预乘冷色整屏 veil,`premultiply` 保证 alpha<250 不可能
饱和成白块。灯笼光晕复核回归 `LanternGlowIsRadialWithoutSquareCorners`:逐像素累积同心圆 alpha,
四角恒为 0、中心行沿半径单调递减,确认无残留方斑(图像级 145+142 帧白块回归 0 命中)。
- **④ HUD 微调**:`HudLayout.textOutlineOffsets`(上下左右 4 偏移)、`HudLayout.segmentBackdrop`
与 `Palette.SegmentBackdrop/Outline`;`DrawHudBar` 每个常驻片段(时段/时间/倍速/体力/钱币)
先铺半透明深色底药丸,再以 1px 深色描边文字,昼/夜两套调色板均不透明描边。回归
`HudPersistentSegmentsGetBackdropAndOutlineInBothPeriods`。
- **回归**:Desktop **136 → 143**,Kernel 92 不变(未触 Kernel/Sim 数值路径,未动 M3)。
- **门槛**:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
3 次一致、`performance_determinism=PASS`;默认世界仍 256×192。
- **取证** `/tmp/opencode/lv-p26-art/`(沙箱把规格路径 `/tmp/lv-p26-art` 映射到此,不放 git):
`p26-map-512x384-seed4242.png`(512 整图真实渲染 3072x2304)、`p26-zoom-480x360-2x.png`
(村庄局部 480×360 裁切 → 2× 最近邻放大,含 30/30 可辨村民)、`p26-day-village.png`、
`p26-night-village.png`(夜景:冷月光 + 桥头灯笼 + HUD)、`p26-hud-day.png`/`p26-hud-night.png`
(HUD 双时段裁切 2×)、`villager-visibility.txt`(autorun 巡游复跑可见村民统计)、
`white-block-day.txt`/`white-block-night.txt`(0 命中)。
- **范围外遗留(另开一单,本单不做)**:① 512 生成耗时因河岸装饰的 O(n²) 间距检查与民居加密从
~80ms 升到 ~151ms(一次性、进入世界前,无逐帧开销),可改空间网格索引优化;② 变体 B 挑檐向两侧
各出一格,相邻民居极近时挑檐可能视觉叠压,可在布点时加 1 格间隔;③ 巡游 40s 档的 `visible>=8`
占比仅 13%(P25 150s 档为 74%),属取证时长差异,非可见性回归。
## P27:Rumor 长局性能修复(工作集扫描去装箱 + 裁剪判定)
- **现状复核(更正 M3-1 前提)**:M3-1 记录的「`World.Rumors` 不裁剪、逐次聊天全表扫描」已由 M6a
(`d94ec2d` 工作集容量上界+按天淘汰、`cc850bf` 窗口化早停扫描)修复:`RumorEvent.DayIndex` 派生字段
+ `trimRumors` 把工作集按「最新在前 + 3 天保留窗口」收敛。实测单日成本已平稳(本机 `--cost-probe`:
1 天 18.7s/18.9GB、5 天 90.5s/95.7GB、10 天 198.2s/191.9GB,≈19GB/天常数),M3 记录的 20 天
≈45 s/天的超线性项已消失。故 P27 不采用「砍 depth/strength 传播」这类会改变模拟语义的方案。
- **剩余 Rumor 成本定位**:工作集有界后每次聊天仍有两条 O(N) 扫描 `latestRumorFor` / `rumorIsDuplicate`;
且此前用 `=` 比较 `[<Struct>]` 的 `NpcId`/`RumorId`,会走泛型结构相等,对每一条被扫描到的谣言
装箱(实测 ≈24 B/条、单次聊天随 N 线性增长——`Sim.chat` 单独测得 N=16000 时 393,736 B/次);
这是 Rumor 逐条分配的主因,而非列表 cons 本身。
- **Kernel 长跑基准**:新增 `src/LivingVillage.Kernel/RumorBench.fs` 与 headless
`--rumor-bench [days npcs chats_per_day]`,把「聊天→生成/追溯谣言」路径从 `Sim.step` 的 NPC 快照
分配解耦,按模拟日推进 tick、固定节奏注入聊天,测工作集分配与耗时;同 `Config` 的
`retained_id_checksum` 可证明确定性。
修复前(30 NPC,`--rumor-bench D 30 2000`):
| days | chat_calls | rumors_retained | allocated_bytes | elapsed_ms |
|---|---|---|---|---|
| 3 | 6000 | 6000 | 930,716,456 | 1626.7 |
| 30 | 60000 | 8000 | 13,419,045,864 | 12182.5 |
| 60 | 120000 | 8000 | 27,297,668,608 | 23769.7 |
60 天工作集稳定在 8000(M6a 的 3 天保留窗口 + 2000/天),不再随年龄增长。
- **修复策略(两条,均不改保留集合与查询结果)**:
1. **扫描去装箱**:`latestRumorFor` / `rumorIsDuplicate` 原来用 `=` 比较 `[<Struct>]` 的
`NpcId`/`RumorId`(含 `RumorId option`),走泛型结构相等、逐条装箱。改为先解构底层
`int`/`int64` 再比较(`Parent` 用 `int64 option` 比较);语义完全等价,扫描不再逐条分配。
2. **裁剪判定 O(1)**:`World` 新增派生镜像 `RumorCount`/`RumorOldestDay`(不参与存档,读档由
`Sim.rumorWorkingSetStats` 从列表重建)。`appendRumor` 用「`count+1 > capacity` 或
`min(oldest, day) < cutoff`」O(1) 判定;旧 `consRumor` 每次追加都 `rumorsNeedTrim` 全表扫一遍。
判定条件与 `rumorsNeedTrim` 逐字等价,仅真需裁剪时才走 `trimRumors`,保留集合逐条相同。
- **修复后基准(同机、同 `--rumor-bench D 30 2000`)**:
| days | 修复前 alloc / ms | 修复后 alloc / ms | alloc 降幅 |
|---|---|---|---|
| 3 | 930,716,456 / 1626.7 | 482,716,192 / 1511.3 | −48% |
| 30 | 13,419,045,864 / 12182.5 | 4,884,067,640 / 8879.6 | −64% |
| 60 | 27,297,668,608 / 23769.7 | 9,774,660,360 / 17078.2 | −64% |
60 天分配 −64%、耗时 −28%;`retained_id_checksum=927964000`(3/30/60 天分别为
17997000 / 447996000 / 927964000)与修复前逐位一致,证明保留集合未变。
- **等价性与门槛**:5 天 2000 聊天/天连跑两遍 `WorldSave.save` 逐字节相同;
`RumorCount`/`RumorOldestDay` 与对列表重算一致(count=8000、oldest_day=1)。
`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
三次一致、`performance_determinism=PASS`;Kernel 92 / Desktop 143 全绿。
- **全模拟口径(`--cost-probe`)**:1 天 18.7s/18.8GB、5 天 90.4s/94.6GB,与修复前基本持平——
全模拟分配由逐 tick 的 `Npc[30]` 快照(`Sim.step` 的 `Array.copy`,≈120 B/NPC/tick)主导,
谣言子系统占比很小;P27 的收益集中在 Rumor 工作集路径,符合「隔离测量」的基准口径。
逐 tick NPC 快照分配属另一个课题(会触及不可变快照不变量),留给后续单。
## P28:512 大图民居观感整改(P26 遗留)
- **① 第二变体辨识度(确定性图集 + 纯绘制分支)**:P26 变体 B 在 512 整图 ~2px/tile 下不可辨,
根因是挑檐与变体 A 同为近黑黛瓦、格栅窗与 A 横向窗差异在 2px 内被抹平。P28 给 B 独立
配色/几何:`scripts/make-jiangnan-art.py` slot28 挑檐改为亮青灰瓦色阶(base 84,106,110 /
light 134,158,158,A 仍是黛瓦脊 slot8),slot29 木格栅窗改竖长比例(内窗 10x18,A 为 18x16
横向),slot30 院门改绿双扇 + 青灰门楣;`VillageArt` 变体 B 再向上叠一层挑檐(该格为水面时
不叠,避免屋顶压水)且向两侧各出一格,墙身统一为格栅窗。图集二次运行 byte-identical
(`jiangnan-world.png` md5 `e5aca186a25b8f8b78d37c0d3ef167b5`)。1 tile≈2px 实测:
A 屋脊 `[46,60,54]` vs B 挑檐 `[68,101,82]`(ΔG≈40),墙线 A 暖 `[154,125,97]` vs
B 冷 `[118,140,115]`,肉眼可分(对照 4x NEAREST 裁剪)。回归
`VariantBRoofPaletteIsClearlyLighterThanVariantA`:解码 world 图集断言 slot28 平均亮度
> slot8 + 15。
- **② 密度/聚落层次(仅大图 mapgen 参数)**:`MapGen.Params.ExtraRiversideHouses` 10 -> 16,
额外民居宽度按 2/3/4 加权(2=附属小筑 / 3=民居 / 4=大宅)。512x384 seed4242 由 18 -> 24 座
(变体 A 11 / B 13;宽度 2×8、3×10、4×6),不再是单一等距 3x2。默认图仍为 0 且不进入
大图分支、不消耗 `structureRng`,故 **64x48=6042052、256x192=1284656962 逐字节不变**
(`DefaultSizeChecksumsArePinned`)。回归 `LargeMapHouseDensityHierarchyOnlyGrowsLargeMaps`。
- **③ 取证钩子(仅证据路径,不改语义)**:`LV_MAPGEN_FIT_WIDTH/HEIGHT` 把整图按指定像素目标
再做一次真实离屏渲染(1024x768 对 512x384 恰为 2px/tile);`LV_MAPGEN_NIGHT=1` 时以 23:30 的
真实 `WorldTint` + golden/moonlight veil 出夜景整图,与 `DrawWorldView` 共用同一套纯函数。
全部为真实渲染帧,非编辑器合成/外表图像。
- **回归/门槛**:Kernel **92** / Desktop **145** 全绿;Release build 0 警告 0 错误;
`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
3 次一致、`performance_determinism=PASS`。
- **取证** `/tmp/opencode/lv-p28/art/`:`p28-map-512x384-seed4242-day.png`(512 整图真实渲染
3072x2304)、`p28-map-512x384-seed4242-2px-day.png`(1024x768 真实帧,1 tile=2px)、
`p28-variant-pair-ab-4x-day.png`(A/B 相邻民居 4x NEAREST 对照,裁 tile x184..214 y186..196)、
`p28-village-core-4x-day.png`(村核 4x)、`p28-village-density-2x-day.png`(密度总览),
以及同名 `*-night.png`(夜景:整图 + 2px + A/B 对照)与 `p28-night-village.png`
(512 夜巡真实帧,HUD 在屏、民居/石板路清晰)。白块回归 4 张夜景整图 + 571 张夜巡帧
0 命中。
## P29:512 聚落组团 + 夜景灯光径向光晕(P28 观感遗留)
- **① 聚落组团(仅大图 mapgen 分支 + 独立 group seed)**:`MapGen.Params` 新增 `ClusterSeed`
(`paramsForSize` 仅大图时置 `seed ^^^ 0xC1A57E2`,默认 0 且非大图不进入分支)。512 沿河民居
不再沿路随机散点,改为沿横向石板路(主街 + 河岸路)成排成组:组内 2-4 座、组内 1 格间隙、
组间留 4-6 格空档,宽度仍按 2/3/4 加权;垂柳改为确定性成丛(2-3 株一丛、丛间 3-5 格空档),
与成排民居呼应。组发牌走独立 `clusterRng`,不触碰既有 `structureRng`,桥/路/灯笼坐标不变;
民居/树仍只落核心区外草地并做重叠检测,BFS 可达性保持。默认图仍 0 额外民居、不消费 RNG,
故 **64x48=6042052、256x192=1284656962 逐字节不变**(`DefaultSizeChecksumsArePinned`)。
- **② 夜景灯光径向衰减(渲染层纯函数)**:新增 `VillageArt.lanternFalloff distance radius`
(半径 0 处 =1,边缘 =0,中间 `1-(d/r)^2` 严格单调、无平台);`lanternGlowLayers` 改为按该衰减
的逐环增量累积 alpha,`drawNightGlows` 对「桥头灯笼 + 民居门窗暖光」(`nightLightTiles`)
画半径 4 tile、8 环的柔和径向光晕。整图取证(`drawMapFitted`/`drawMapViewport`)与实机
`DrawWorldView` 共用同一套函数;无硬方斑、无白块(`check-no-white-blocks` 11 帧 0 命中)。
- **③ 取证钩子**:复用 `LV_MAPGEN_SHOT` + `LV_MAPGEN_FIT_WIDTH/HEIGHT`(2/4/8 px per tile)与
`LV_MAPGEN_NIGHT=1`(23:30 真实 `WorldTint` + golden/moonlight veil);夜景帧在原整图流程上
叠加与实机一致的灯光光晕。
- **回归**:Kernel 92 / Desktop 151(+6:`ClusterSeedOnlyEnabledForLargeMaps`、
`LargeMapHousesFormClusteredRowsWithControlledGaps`、`LargeMapClusterLayoutChecksumIsEmptyDiffAcrossRuns`、
`LargeMapWillowsGrowInGroves`、`LanternFalloffIsMaxAtCoreAndMonotonicWithoutPlateau`、
`NightLightTilesCombineBridgeLanternsAndHouseWindows`)全绿;Release 0/0;
`final_digest=953775FAEB2F…` 3 次一致、`performance_determinism=PASS`。
- **取证** `/tmp/opencode/lv-p29/art/`:`p29-map-512x384-seed4242-{day,night}.png`(512 整图
3072x2304 真实渲染)、`p29-village-cluster-4x-{day,night}.png`(512x256,4px/tile 桥头沿河组团
局部,含成排民居/桥/水面)、`p29-lantern-halo-8x-night.png`(192x192,单灯 8px/tile 特写;
径向剖面 191→189→183→167→137→113→96→84 平滑递减)、`p29-variant-pair-ab-4x-{day,night}.png`
(512x256,A/B 对照);
源帧 `p29-day/`、`p29-night/`、`p29-day8/`、`p29-night8/`;工作区 `art/` 不入 git。
- **遗留**:民居总数与 P28 相同(24 座),本轮只做「组团化」未继续提高总密度;河岸垂柳成丛后
仍为纯视觉层,不参与通行;默认 64x48/256x192 观感不变。
## P30:512 民居增密 + 乡村空区填充 + 主路柔化(P29 观感遗留)
- **① 民居增密与组团(仅大图 mapgen 分支)**:`ExtraRiversideHouses` 16 -> 30,组团规模由
2-4 改为 2-5 座;含基础岸屋的横向路段优先布组,并为每座仍孤立的基础民居在同行贴 1 格补
一座。`placeGroup` 先局部试排,再用 `connected`(与既有民居或同组民居足迹间隙 <=2)过滤后
才提交,孤立单屋根本不落地。512x384 seed4242:沿河/沿路组团民居 24 -> **45 座**,组团规模
`[5;4x6;3x2;2x3]`,**0 组 <2 座**;宽度仍按 2/3/4 加权(附属小筑/民居/大宅)。
- **② 乡村空区填充(仅大图)**:新增 `GroundTile.PaddyField=4 / VegetablePlot=5`(成熟 tile
素材:水田走静态水面 sprite、菜畦走菜地 sprite;均为非水瓦片故仍可通行)、`Result.Groves`
(竹丛/灌木丛,纯视觉层,不入瓦片、不影响通行)、`Result.Farmhouses`(散落农舍,门贴院落路,
计入 BFS 可达性)。按 18x14 确定性网格铺田块/树丛/农舍,田块离水面 >=3 格并避开路网/核心/
民居;上部与下部空带均有田块。512x384 seed4242:水田 11595 + 菜畦 12877 格、树丛 852 格、
散落农舍 21 座;草地瓦片占比 **96.3% -> 83.9%**(整图真实渲染中草地像素约 80.8%)。
- **③ 主路柔化(仅大图)**:新增 `Result.MainRoadCenter` 逐列中心线,用确定性 value noise
(scale 130、幅度 ±3 格)微扰,相邻列步长恒 `<=1`,遇弯补一格保证 4 连通;桥的竖向引路按
`roadYAt bx` 对齐。默认图主街仍笔直。同 seed 两次逐字节一致(`serialize` 含中心线)。
- **回归 +5**:`LargeMapGroupsAreTwoToFiveWithFewIsolated`、
`LargeMapCountrysideFillsEmptyBandsWithMatureTiles`、`FieldsStayOnLandAwayFromWaterPathsAndCore`、
`LargeMapFarmsteadDoorsAreReachableAndDeterministic`、`LargeMapMainRoadIsDeterministicSerpentine`。
Kernel **92** / Desktop **156** 全绿;Release build 0 警告 0 错误;
`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C` 3 次一致、
`performance_determinism=PASS`;**64x48=6042052、256x192=1284656962 锁定不变**
(`DefaultSizeChecksumsArePinned`)。
- **取证** `/tmp/opencode/lv-p30/art/`(真实离屏渲染,非构造):`p30-map-512x384-seed4242-{day,night}.png`
(3072x2304 整图)、`p30-field-cluster-4x-{day,night}.png`(512x256,4px/tile,田块+农舍+树丛)、
`p30-roadserp-4x-day.png`(512x256,4px/tile,主路逐点弯曲)。`check-no-white-blocks` 5 帧 0 命中。
- **遗留**:散落农舍与乡村树丛为纯布局/美术层,不与 NPC 数值、任务系统耦合;默认
64x48/256x192 判定不进入新分支、不消费额外 RNG,观感不变。
## 已验证命令
```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
# P27 Rumor 长跑基准(隔离谣言子系统;默认 30 NPC×60 天×2000 聊天/天)
dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll --rumor-bench
dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll --rumor-bench 3 30 2000
# 素材再生成(确定性,输出 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
# P16-FIX 开始界面截图取证(停在主菜单,拍初始/读档高亮与 splash 淡入中帧)
timeout 90 xvfb-run -a -s "-screen 0 1280x720x24" env \
LV_AUTOPLAY_MENU_SHOT=1 LV_RECORD_DIR=/tmp/opencode/lv-p16-art \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# P17 取证(街边细节/人物节奏/昼夜精修;LV_AUTOPLAY_START_HOUR 定起时:昼 12 / 黄昏 20.75 / 夜 0)
for pair in day:12.0 dusk:20.75 night:0.0; do name=${pair%%:*}; hour=${pair##*:}; \
timeout 100 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_AUTOPLAY_START_HOUR=$hour LV_AUTOPLAY_RECORD=1 \
LV_RECORD_DIR=/tmp/opencode/lv-p17-art/$name LV_RECORD_EVERY=2 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
# P19 白天色温取证(正午 12 / 黄昏 20.75 / 深夜 0.5 三帧对比)
for pair in noon:12.0 dusk:20.75 night:0.5; do name=${pair%%:*}; hour=${pair##*:}; \
timeout 100 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_AUTOPLAY_START_HOUR=$hour LV_AUTOPLAY_RECORD=1 \
LV_RECORD_DIR=/tmp/opencode/lv-p19-art/$name LV_RECORD_EVERY=2 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
# P21 性能核对:256x192 可玩世界实机帧率(放开 vsync)+ 60s 稳定采样
timeout 90 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_RIVERSCAPE_TOUR=1 LV_RIVERSCAPE_SECONDS=60 LV_UNLOCK_FPS=1 LV_PERF_SUMMARY=1 \
LV_AUTOPLAY_START_HOUR=12.0 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# 内存(运行中采样 VmRSS)与 headless ticks/s
PID=$(pgrep -n -f LivingVillage.Desktop.dll); awk '/VmRSS/{print $2" kB"}' /proc/$PID/status
dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll --performance-baseline
# P21 昼夜巡游取证(真走位,p21-* 命名;代表帧挑出到 /tmp/opencode/lv-p21-art/)
for pair in day:12.0 night:20.75; do name=${pair%%:*}; hour=${pair##*:}; \
timeout 150 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_RIVERSCAPE_TOUR=1 LV_AUTOPLAY_RECORD=1 LV_RECORD_EVERY=20 LV_RECORD_NAME=p21-$name- \
LV_RECORD_DIR=/tmp/opencode/lv-p21-art/raw-$name LV_AUTOPLAY_START_HOUR=$hour \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
# P22 补夜档巡游(23:30),并从中重挑「游戏内」代表帧覆盖启动/标题画面误帧
# HUD 判定:y in [2,35) 且 x in [0,w/2) 的近白像素 > 500 => 游戏内帧(标题/启动为 0)
timeout 150 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_RIVERSCAPE_TOUR=1 LV_AUTOPLAY_RECORD=1 LV_RECORD_EVERY=40 LV_RECORD_NAME=p21-n2- \
LV_RECORD_DIR=/tmp/opencode/lv-p21-art/raw-night2 LV_AUTOPLAY_START_HOUR=23.5 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# P22 默认评估:64x48 样例世界 60s 帧率采样(LV_PERF_SOAK_SECONDS 让脚本循环采样)
timeout 120 xvfb-run -a -s "-screen 0 1280x720x24" env LV_LEGACY_MAP=1 \
LV_AUTOPLAY_SAMPLE=1 LV_UNLOCK_FPS=1 LV_PERF_SUMMARY=1 LV_PERF_SOAK_SECONDS=60 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# P23 512x384 扩程取证:整图真实渲染 + 视口/近景(seed 4242 与 7)
for seed in 4242 7; do timeout 120 xvfb-run -a -s "-screen 0 1280x720x24" env \
LV_MAPGEN_SHOT=1 LV_MAPGEN_WIDTH=512 LV_MAPGEN_HEIGHT=384 LV_MAPGEN_SEED=$seed \
LV_RECORD_DIR=/tmp/opencode/lv-p23-art \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
# P23 512x384 巡游帧率(单趟,解锁 vsync;日志打印 perf-summary 与 VmRSS 可另采)
timeout 280 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_RIVERSCAPE_TOUR=1 LV_RIVERSCAPE_WIDTH=512 LV_RIVERSCAPE_HEIGHT=384 \
LV_UNLOCK_FPS=1 LV_PERF_SUMMARY=1 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# P24 512x384 民居密度取证:整图 + 巡游(记录帧;LV_NPC_VISIBLE_LOG 打印可见村民数)
timeout 240 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_RIVERSCAPE_TOUR=1 LV_RIVERSCAPE_WIDTH=512 LV_RIVERSCAPE_HEIGHT=384 \
LV_UNLOCK_FPS=1 LV_NPC_VISIBLE_LOG=1 LV_AUTOPLAY_RECORD=1 LV_RECORD_EVERY=120 \
LV_RECORD_NAME=p24-tour- LV_RECORD_DIR=/tmp/opencode/lv-p24-art/tour512 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# 局部放大由 tour512 帧裁 480x360 后 2x 最近邻放大(含民居+村民)
# P25 村民可见性巡游:路点在 Sim 四个活动点间环游,150s 循环,多数采样帧 >=8 村民
timeout 220 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_RIVERSCAPE_TOUR=1 LV_RIVERSCAPE_VILLAGE_TOUR=1 LV_RIVERSCAPE_WIDTH=512 \
LV_RIVERSCAPE_HEIGHT=384 LV_RIVERSCAPE_SECONDS=150 LV_UNLOCK_FPS=1 \
LV_NPC_VISIBLE_LOG=1 LV_AUTOPLAY_RECORD=1 LV_RECORD_EVERY=120 \
LV_RECORD_NAME=p25-tour- LV_RECORD_DIR=/tmp/opencode/lv-p25-art/tour \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# P25 整图取证(无孤立灰石板)
timeout 120 xvfb-run -a -s "-screen 0 1280x720x24" env LV_MAPGEN_SHOT=1 \
LV_MAPGEN_WIDTH=512 LV_MAPGEN_HEIGHT=384 LV_MAPGEN_SEED=4242 \
LV_RECORD_DIR=/tmp/opencode/lv-p25-art \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# P26 512 整图真实渲染(含两种民居立面 + 河岸装饰;产物 p20b-map-*.png,改名为 p26-map-*)
timeout 150 xvfb-run -a -s "-screen 0 1280x720x24" env LV_MAPGEN_SHOT=1 \
LV_MAPGEN_WIDTH=512 LV_MAPGEN_HEIGHT=384 LV_MAPGEN_SEED=4242 \
LV_RECORD_DIR=/tmp/opencode/lv-p26-art/map \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# P26 昼夜村庄巡游录帧(村民可见性日志 + 夜景月光/灯笼/HUD 取证)
for pair in day:12.0 night:23.5; do name=${pair%%:*}; hour=${pair##*:}; \
timeout 200 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_RIVERSCAPE_TOUR=1 LV_RIVERSCAPE_VILLAGE_TOUR=1 LV_RIVERSCAPE_WIDTH=512 \
LV_RIVERSCAPE_HEIGHT=384 LV_RIVERSCAPE_SECONDS=40 LV_AUTOPLAY_START_HOUR=$hour \
LV_NPC_VISIBLE_LOG=1 LV_AUTOPLAY_RECORD=1 LV_RECORD_EVERY=30 \
LV_RECORD_NAME=p26-$name- LV_RECORD_DIR=/tmp/opencode/lv-p26-art/$name \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
# 局部 480x360 锚定村民+民居质心裁切后 2x 最近邻放大 -> p26-zoom-480x360-2x.png
# HUD 双时段:裁顶部 y 0..44 左 640px 再 2x -> p26-hud-day.png / p26-hud-night.png
# P26 夜景白块回归(方斑核对,0 命中)
python3 scripts/check-no-white-blocks.py /tmp/opencode/lv-p26-art/night
# P28 512 整图真实渲染(3072 整图 + 1024x768 恰 2px/tile;night=1 走 23:30 tint+月光 veil)
for night in 0 1; do extra=""; [ "$night" = 1 ] && extra="LV_MAPGEN_NIGHT=1"; \
dir=/tmp/opencode/lv-p28/art/map; [ "$night" = 1 ] && dir=/tmp/opencode/lv-p28/art/map-night; \
timeout 180 xvfb-run -a -s "-screen 0 1280x720x24" env LV_MAPGEN_SHOT=1 \
LV_MAPGEN_WIDTH=512 LV_MAPGEN_HEIGHT=384 LV_MAPGEN_SEED=4242 \
LV_MAPGEN_FIT_WIDTH=1024 LV_MAPGEN_FIT_HEIGHT=768 $extra LV_RECORD_DIR=$dir \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
# 两个变体 4x NEAREST 裁剪对照(从 1024x768 真实帧按 2px/tile 裁 tile x184..214 y186..196)
python3 - <<'PY'
from PIL import Image
for tag, p in (("day","/tmp/opencode/lv-p28/art/map"), ("night","/tmp/opencode/lv-p28/art/map-night")):
im = Image.open(f"{p}/p28-fit-1024x768-seed4242.png").convert("RGB")
c = im.crop((184*2, 186*2, 214*2+2, 196*2+2))
c.resize((c.width*4, c.height*4), Image.NEAREST).save(f"/tmp/opencode/lv-p28/art/p28-variant-pair-ab-4x-{tag}.png")
PY
# P28 夜景白块回归(4 张整图 + 571 张夜巡帧,0 命中)
python3 scripts/check-no-white-blocks.py /tmp/opencode/lv-p28/art/map-night
python3 scripts/check-no-white-blocks.py /tmp/opencode/lv-p28/art/night-raw
# P28 512 夜巡真实帧(seed 42;默认桥/门巡游路点,夜景 23:30)
timeout 220 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_RIVERSCAPE_TOUR=1 LV_RIVERSCAPE_WIDTH=512 LV_RIVERSCAPE_HEIGHT=384 \
LV_RIVERSCAPE_SECONDS=60 LV_AUTOPLAY_START_HOUR=23.5 LV_NPC_VISIBLE_LOG=1 \
LV_AUTOPLAY_RECORD=1 LV_RECORD_EVERY=20 LV_RECORD_NAME=p28-night- \
LV_RECORD_DIR=/tmp/opencode/lv-p28/art/night-raw \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# 代表帧 p28-night-10100.png -> art/p28-night-village.png
# P29 512 组团 + 夜景光晕取证(4/8 px per tile;night 走 23:30 tint+veil+径向光晕)
run_p29 () { # $1=fit_w $2=fit_h $3=night(0/1) $4=out_dir
local extra=""; [ "$3" = 1 ] && extra="LV_MAPGEN_NIGHT=1"
timeout 300 xvfb-run -a -s "-screen 0 1280x720x24" env LV_MAPGEN_SHOT=1 \
LV_MAPGEN_WIDTH=512 LV_MAPGEN_HEIGHT=384 LV_MAPGEN_SEED=4242 \
LV_MAPGEN_FIT_WIDTH=$1 LV_MAPGEN_FIT_HEIGHT=$2 $extra LV_RECORD_DIR=$4 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
}
run_p29 2048 1536 0 /tmp/opencode/lv-p29/art/p29-day
run_p29 2048 1536 1 /tmp/opencode/lv-p29/art/p29-night
run_p29 4096 3072 0 /tmp/opencode/lv-p29/art/p29-day8
run_p29 4096 3072 1 /tmp/opencode/lv-p29/art/p29-night8
# 裁剪(PIL):整图 p20b-map-*.png -> p29-map-*;组团 4x = 2048 帧裁 tile x278..406 y82..146;
# 单灯 8x = 4096 帧以桥灯 (113,180) 为中心裁 192x192;
# A/B 对照 = 2048 帧裁 tile x228..260 y170..186 后 4x NEAREST -> 512x256
python3 scripts/check-no-white-blocks.py /tmp/opencode/lv-p29/art/p29-night /tmp/opencode/lv-p29/art/p29-night8
# P30 512 证据出图(真实离屏渲染:整图 3072x2304 + 田块/主路局部 512x256@4px per tile)
mkdir -p /tmp/opencode/lv-p30/art
timeout 300 xvfb-run -a -s "-screen 0 1280x720x24" env LV_MAPGEN_SHOT=1 LV_MAPGEN_P30_SHOT=1 \
LV_MAPGEN_WIDTH=512 LV_MAPGEN_HEIGHT=384 LV_MAPGEN_SEED=4242 \
LV_RECORD_DIR=/tmp/opencode/lv-p30/art \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
python3 scripts/check-no-white-blocks.py /tmp/opencode/lv-p30/art/*.png
# P20 第三步:默认生成器世界的昼夜巡游录制(真走位,含桥面行走帧;p20c-* 命名)
for pair in day:12.0 night:20.75; do name=${pair%%:*}; hour=${pair##*:}; \
timeout 150 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_RIVERSCAPE_TOUR=1 LV_AUTOPLAY_RECORD=1 LV_RECORD_EVERY=20 LV_RECORD_NAME=p20c-$name- \
LV_RECORD_DIR=/tmp/opencode/lv-p20-art/p20c-$name LV_AUTOPLAY_START_HOUR=$hour \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
# P20 大地图生成器取证(整图 + 裁剪帧;每 seed 一拍即退,日志打印 gen_ms/drawn/culled)
for seed in 4242 7 99; do timeout 120 xvfb-run -a -s "-screen 0 1280x720x24" env \
LV_MAPGEN_SHOT=1 LV_MAPGEN_WIDTH=256 LV_MAPGEN_HEIGHT=192 LV_MAPGEN_SEED=$seed \
LV_RECORD_DIR=/tmp/opencode/lv-p20-art \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
# P18 黄昏暖色取证(20:45 真实帧;深夜把 20.75 换成 0)
timeout 100 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_AUTOPLAY_START_HOUR=20.75 LV_AUTOPLAY_RECORD=1 \
LV_RECORD_DIR=/tmp/opencode/lv-p18-art/dusk LV_RECORD_EVERY=2 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
# 街边细节图集再生成(确定性,二次运行 byte-identical)
python3 scripts/make-jiangnan-art.py
# 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-p25-art/`(P25:`p25-map-512x384-seed4242.png` 整图无灰石板、
`p25-culled-*`/`p25-detail-*`、`p25-zoom-village.png` 放大含 >=8 村民、
`p25-tour-0{1,2,3,4}.png` tour 页、`tour/` 全量巡游帧)
- `/tmp/opencode/lv-p24-art/`(P24 512x384:`p24-map-512x384-seed{4242,7}.png` 整图、
`p24-zoom-houses.png`/`p24-zoom-villagers.png` 局部放大、`p24-tour-0{1,2,3}.png` tour 页、
`tour512/` 全量巡游帧;规格要求的 `/tmp/lv-p24-art` 被沙箱 external_directory deny,落此路径)
- `/tmp/opencode/lv-p23-art/`(P23 512x384:`p23-map-512x384-seed{4242,7}.png` 整图真实渲染、
`p23-culled-512x384-seed{4242,7}.png` 视口裁剪、`p23-detail-512x384-seed{4242,7}.png` 1:1 近景、
`tour512/` 巡游帧)
- `/tmp/opencode/lv-p21-art/`(P21:`p21-day-bridge-01.png`、`p21-day-river-01.png`、
`p21-day-buildings-0{1,2}.png`、`p21-night-bridge-01.png`、`p21-night-river-01.png`、
`p21-night-street-01.png`;原始帧 `raw-day/`、`raw-night/`)
- `/tmp/opencode/lv-p20-art/`(P20 第三步:默认生成器世界昼夜巡游 `p20c-day-*.png`/`p20c-night-*.png`
(含桥面行走)、代表帧 `p20c-day-bridge-01.png`/`p20c-day-buildings-01.png`/
`p20c-night-bridge-01.png`/`p20c-night-lantern-01.png`;P20:第一步 `mapgen-256x192-seed{4242,7,99}.png` 整图、
`mapgen-culled-256x192-seed{4242,7,99}.png` 裁剪帧;第二步 `p20b-map-256x192-seed{4242,7,99}.png`
整图(含桥/路/沿河民居)、`p20b-detail-256x192-seed{4242,7,99}.png` 桥/路/民居 1:1 近景、
`p20b-culled-256x192-seed{...}.png` 裁剪帧)
- `/tmp/opencode/lv-p19-art/`(P19:`p19-noon-12h-0{1,2}.png`、`p19-dusk-20h45-01.png`、
`p19-night-00h30-01.png`、`p19-before-noon-yellow.png`/`p19-after-noon-neutral.png`;同一份拷入 `evidence/`)
- `/tmp/opencode/lv-p18-art/`(P18:`p18-dusk-20h45-0{1,2}.png`、`p18-night-deep-01.png`、
`p18-before-old-dusk-blue.png`/`p18-after-new-dusk-warm.png`、`sample-dusk.mp4`;同一份拷入 `evidence/`)
- `/tmp/opencode/lv-p17-art/`(P17:`p17-day-12h-0{1,2}.png`、`p17-dusk-20h45-0{1,2}.png`、
`p17-night-00h-0{1,2}.png`、`p17-diff-before-dawn.png`/`p17-diff-after-dawn.png`、`sample.mp4`;
同一份拷入 `evidence/`)
- `/tmp/opencode/lv-p16-art/`(P16:`frame-day-0{1,2,3}.png`、`frame-night-0{1,2,3}.png`、
`frame-splash-01.png`、`frame-menu-0{1,2}.png`、`hud-bar-day.png`、`hud-bar-night.png`、
`sample.mp4`、`final_verification.txt`;沙箱不允许 `/tmp/lv-p16-art`,故落此路径)
- `/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/`)
- `/tmp/opencode/lv-p28/art/`(P28:`p28-map-512x384-seed4242-day.png`/`-night.png` 512 整图、
`p28-map-512x384-seed4242-2px-day.png`/`-night.png` 1024x768 恰 2px/tile、
`p28-variant-pair-ab-4x-day.png`/`-night.png` A/B 4x 对照、
`p28-village-core-4x-day.png`/`-night.png`、`p28-village-density-2x-day.png`/`-night.png`、
`p28-night-village.png` 夜巡代表帧、`night-raw/` 571 帧;工作区内不入 git)
- `/tmp/opencode/lv-p29/art/`(P29:`p29-map-512x384-seed4242-{day,night}.png` 512 整图、
`p29-village-cluster-4x-{day,night}.png` 512x256 桥头沿河组团 4px/tile、
`p29-lantern-halo-8x-night.png` 192x192 单灯 8px/tile 光晕特写、
`p29-variant-pair-ab-4x-{day,night}.png` 512x256 A/B 对照;源帧 `p29-day/`、`p29-night/`、
`p29-day8/`、`p29-night8/`;工作区 `art/` 不入 git)
- `/tmp/opencode/lv-p30/art/`(P30:`p30-map-512x384-seed4242-{day,night}.png` 3072x2304 整图、
`p30-field-cluster-4x-{day,night}.png` 512x256 田块+农舍+树丛 4px/tile、
`p30-roadserp-4x-day.png` 512x256 主路逐点弯曲 4px/tile;工作区 `art/` 不入 git)
- `artifacts/perf-independent/<TS>/`(digest 3 联测回读)
|