summaryrefslogtreecommitdiff
path: root/docs/api-reference/rendering-desktop.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/api-reference/rendering-desktop.md')
-rw-r--r--docs/api-reference/rendering-desktop.md65
1 files changed, 65 insertions, 0 deletions
diff --git a/docs/api-reference/rendering-desktop.md b/docs/api-reference/rendering-desktop.md
new file mode 100644
index 0000000..ca3a841
--- /dev/null
+++ b/docs/api-reference/rendering-desktop.md
@@ -0,0 +1,65 @@
+# 桌面渲染(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`,仅日志、不改渲染逻辑)。