# 桌面渲染(Game / VillageArt / 字形图集) 源码:`src/LivingVillage.Desktop/Game.fs`、`src/LivingVillage.Desktop/VillageArt.fs`、 `src/LivingVillage.Desktop/CjkGlyphAtlas.fs`、`src/LivingVillage.Desktop/ChineseText.fs`、 `src/LivingVillage.Desktop/HudLayout.fs`。 ## 职责 MonoGame 渲染与主循环:把 `World` 画成江南水乡画面(地面/河道/建筑/人物/夜景光晕/室内), 并绘制 HUD、对话浮层与各类面板。不负责模拟推进(`Sim.step` 在 Kernel)、不负责地图生成算法。 ## 对外 API 概览 - `LivingVillageGame`(`Game` 子类):`override Update(gameTime)` / `override Draw(gameTime)`。 默认 `IsFixedTimeStep = true`、`TargetElapsedTime = 1/60s`、垂直同步开启; `LV_UNLOCK_FPS=1` 时放开两者以测渲染上限。 - `VillageArt.loadTextures device` → `ArtTextures`;`VillageArt.drawWorld`、 `VillageArt.drawMapViewport` / `drawMapFitted` / `drawMapRegion`、`VillageArt.drawCharacter`、 `VillageArt.drawInterior` / `drawInteriorCharacter`、`VillageArt.drawNightGlows`、 `VillageArt.drawCc0BoatOverlay`、`VillageArt.drawGroundShadow`。 - 动画:`VillageArt.characterFrame isMoving tick`、`npcSpriteSpec*`、`avatarSpriteSpec`、 `waterFrameTickAt tick x y`、`smokeFrameTick tick`。 - 预乘与光晕:`VillageArt.premultiply r g b a`、`VillageArt.lanternGlowLayers`、 `VillageArt.ellipseRuns`。 - 中文字形:`CjkGlyphAtlas.columns = 16`、`cellPixels = 24`、`contains char`、 `sourceRectangle char`、`load device`;文案清单 `ChineseText.requiredUiLabels`; HUD 几何 `HudLayout.barHeight = 36`、`textScale = 2`。 ## 单位与量纲 - 视口默认 1280×720;世界像素 = 瓦片 × `Sim.tilePixels`(32)。 - 字形:每字一格 24×24 px,绘制时按 `PixelText` 的 `scale` 放大(HUD 常用 `scale = 2` → 每字 16px; 图集源格固定 24px)。ASCII 走 `PixelText.glyphs` 位图,空格宽 `4*scale`,其余 ASCII 宽 `6*scale`。 - 动画:字符格 32×48;`animationFrame = (tick/4) % 4`、`idleFrame = (tick/24) % 2`。 ## 不变量 1. **预乘 alpha**:任何 `alpha < 255` 的前景绘制必须用 `VillageArt.premultiply`, 否则预乘混合会把 rgb 直接加进帧缓冲导致纯白块(见 `docs/维护说明.md` §预乘 alpha)。 2. **无缺字**:`PixelText.isRenderable` 要求每个字符 ∈ ASCII 位图 ∪ `CjkGlyphAtlas.contains`; 不可渲染字符按设计**留空**,绝不画假字。 3. **图集几何**:`CjkGlyphAtlas` 表保持有序稳定,`pixelWidth = 16*24 = 384`、 `pixelHeight = rowCount*24`;`load` 校验尺寸否则 `invalidOp`。 4. **帧选择纯函数**:动画/水波/炊烟只由 `tick` 与「是否移动」决定,同 tick 必得同帧。 5. **绘制分层**:`drawWorld` 末尾单独绘制炊烟;夜间光晕只在真实灯笼实体处产生。 ## 失败分支 - `CjkGlyphAtlas.load`:文件缺失 → `FileNotFoundException`;尺寸不符 → `invalidOp` 并释放纹理。 - 文本:`PixelText.draw` 对不支持字符静默跳过(推进光标),不抛异常。 - 纹理加载:`VillageArt.loadTexture` 校验期望宽高,资产缺失/尺寸不符会失败(构建期资产必在)。 ## 算法与性能取舍 - 绘制分层(`Game.DrawWorldView`):地面 → 结构/民居 → 夜间光晕 → 水面波纹/船 → 角色阴影与角色 → 黄金时刻暖幕 → 月光幕 → 面板/浮层(`DrawM5Overlay`)。 - 只绘制可见瓦片(`isVisible` 视口裁剪);光晕用预乘同心圆 pixel run 近似径向衰减。 - 帧循环结构:`Update`(键盘/菜单/证据钩子/`Sim` 推进)→ `Draw`(世界或菜单/证据帧); 证据钩子(`LV_*_SHOT`)在 Playing 帧后落盘并退出。 - 实测(`docs/evidence/p70-analysis.txt`,1280×720 / Xvfb 软件渲染 / 解锁 vsync): 60 秒 8,788 帧、`measured_fps ≈ 139.3`;帧间隔 mean 7.179 / max 33.919 / p95 9.847 ms; 掉帧 > 25ms = 4(0.05%)、> 33.3ms = 2(0.02%);每帧分配 mean ≈ 231 KB / p95 297 KB。 帧率随机器与软件渲染波动,只列本次实测,不外推。 - 复跑:`P70_SECONDS=60 bash scripts/bench-p70.sh /tmp/opencode/lv-p70` (逐帧日志经 `LV_FRAME_LOG=1`,仅日志、不改渲染逻辑)。