0.2.6-dev: DESIGN §31.6 for the impostor box without the setup pose (slots 1.26× the old area on the roster, the DPR-2 crowd numbers, the batch-buffer setting; the open item resolved), docs/design/client.md, CHANGELOG 0.2.6-dev (#542); golden: unchanged
This commit is contained in:
1 parent
c973c1a1ad
commit
4c2fe9431d
3 files changed
+43
-36
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
### 界面
|
||||
- 最后一个还没选策略的玩家不再看到可点的「跳过」按钮:已经选完的玩家仍显示在顺序里,但不算可以让过去的人(PR #546,感谢 @Xinglan233)。
|
||||
- 场上单位多时(画面质量「高」下通常在第 7 回合以后)模型改用缓存画面绘制,此前野鬃待机时的旗帜、浊心斯卡蒂三技能飞起时的头部会被裁掉,超出的部分还会画进相邻单位的缓存,在隐现的枪口等处留下错位的残影。现在每个模型按它会播放的全部动作预留缓存大小,每个单位只画在自己的区域内;少数特别大的动作(多为部署时从天而降)播放期间直接绘制(#542,感谢 @ImPQlc)。
|
||||
- 场上单位多时(画面质量「高」下通常在第 7 回合以后)模型改用缓存画面绘制,此前野鬃待机时的旗帜、浊心斯卡蒂三技能飞起时的头部会被裁掉,超出的部分还会画进相邻单位的缓存,在隐现的枪口等处留下错位的残影。现在每个模型按它会播放的全部动作预留缓存大小,每个单位只画在自己的区域内;少数特别大的动作(多为部署时从天而降)播放期间直接绘制;单位在眩晕中才加载完成的模型也不再显示初始姿势(#542,感谢 @ImPQlc)。
|
||||
|
||||
### 工具与开发
|
||||
- Windows 下场景素材测试的仓库路径不再重复盘符(PR #547,感谢 @Xinglan233)。
|
||||
|
||||
@@ -42,7 +42,7 @@ view.resize() ; view.destroy()
|
||||
|
||||
Asset URLs (0.2.6, host bandwidth layer 1): the client takes every art, model and audio URL from `data/assets.json` and `data/local-assets.json` and builds none itself, so the content-hash segment the server puts in front (`/assets/~<hash>/…`, docs/ASSETS.md "The committed manifest and the served manifests") needs nothing on this side: `validSpine` still sees `.skel`, pixi-spine finds the atlas and its pages under the same segment (a model shares one hash), audio is fetched as `/media/~<hash>/…`. The two URLs the client derives from another file's — the 3D board's `tiles.json` beside the atlas and the token death sound beside `charDie` — are served `no-cache` (the board) or drop the segment (`shared/assetPaths.js unhashedUrl`, the sound).
|
||||
|
||||
Visuals: oblique perspective board (tilted ~30°, trapezoid tiles), raised highland blocks with side faces, road/floor/forbidden styles per research 07 §7, pulsing red gates, blue objective, dashed hand pads. Units: Spine (Front/Back rule, feet anchored — flying units hover `FLY_HOVER` 1.3 tiles up, the client's 0.35 in character space, research 12, §25.13.5 — an enemy flyer from the road whatever tile it crosses (GitHub #277) —, shadow sprite on the ground), elite gold outline glow, tier chip, HP bar (green ally / red enemy), SP bar (skill ready glow; while an ammo skill runs it is the magazine instead — a yellow cell per round, emptying from the right, `b.snap ammo`, and the skill draws no standing aura, UnitInfo `ammoSkill`), 斩业星熊's 我执 pool as a red bar over her drained HP bar (`b.snap neg`), 伺夜's 狼群 count as a row of diamonds under the bars, the lit ones the 狼影 left (`b.snap wolves`; 0.2.2, GitHub PR #303 / #378 / #383), status icons, the element gauge row under the bars — icon + white bar of the remaining 元素值, element-coloured refill during a 爆发冷却 (§18.3, §20.8); enemies drawn × their official prefab factor (enemies.json `modelScale`, §20.4); an enemy's mode — the `form` of a sim fx (`shared/protocol.js fxForm`), or UnitInfo `form` for a view built later through `render/app/info.js renderInfo` — switches clip sets (`render/units.js FORMS`: 掠海漂移体 crawl, 暴鸰 bombed, 转译基底's forms, the 逐火 embers, the leaders' 重生, 守墓石像, the 孤岛风云 prisoners' warning and 解放; closing clips timed from the fx `dur`), a one-off cast (`PROJ[kind].once`) plays once, and a local-only model's web alias is tinted (`ALIAS_TINT`: 灼热 / 炽焰源石虫, §21.14); damage numbers (phys orange-white, arts purple, true white, heal green); projectiles drawn per kind, hit sparks and melee slashes, skill activation bursts and auras (§17.3); death fade for enemies, summons and devices — a knocked-out operator stays on its tile in its held Die pose under a redeploy ring (§18.3); leak flash at the objective. Fallback when Spine missing/failed: avatar in a rarity-coloured diamond with bob/lunge tweens — a failed or timed-out model is loaded again (bounded), and a view re-resolves its model and avatar when the asset manifest arrives late or the tab is shown again (§21.27). 60 fps target with ≤ 120 units (a crowd draws its Spine units through the cached atlas, `render/impostor.js`, each refreshed every k-th frame: the refresh turns follow this frame's update order — `ctx.impostorSlot()`, a counter reset at the start of every frame — so a frame refreshes ⌊n/k⌋ or ⌈n/k⌉ of n units instead of whatever random per-unit phases gave, and a unit not refreshed for 2k frames is refreshed regardless; GitHub PR #334 by @Cloudnyco, issue #327 step 1 — its step 2, a time budget by priority, is not done; a model whose pose cannot change — `SpineActor.poseHeld()`: a knocked-out operator in the held end of its Die clip, a frozen model — is neither re-posed nor redrawn, only its clock runs, until a new clip, a revive, a wind-up, a form's closing clip, a clipping switch or a zoom past the impostor's rescale threshold; an impostor's slot is sized from the unit's setup pose and every clip it can play — measured once per skeleton, a few frames after the model loads (`render/spine.js clipBounds`, sampled within a 2.5 ms frame budget; until then the unit is drawn directly) —, a clip too large for that box (`render/units.js impostorBox`: mostly a deploy drop from above) is drawn directly while it plays, and the atlas draws every slot clipped to its own rectangle (a scissor rect), so no pose is cut and nothing is left in another unit's slot (GitHub #542, §31.6); the redeploy rings draw one shared rasterised label per number, `render/textures.js downLabel`, the same pixels as a Text of their own). Picking (which unit a press / hover / dropped item is on) and the drag target follow one rule, `render/pick.js` — the tile under the pointer (§18.1); the Spine store never hands out a skeleton whose unload is in flight (§17.1).
|
||||
Visuals: oblique perspective board (tilted ~30°, trapezoid tiles), raised highland blocks with side faces, road/floor/forbidden styles per research 07 §7, pulsing red gates, blue objective, dashed hand pads. Units: Spine (Front/Back rule, feet anchored — flying units hover `FLY_HOVER` 1.3 tiles up, the client's 0.35 in character space, research 12, §25.13.5 — an enemy flyer from the road whatever tile it crosses (GitHub #277) —, shadow sprite on the ground), elite gold outline glow, tier chip, HP bar (green ally / red enemy), SP bar (skill ready glow; while an ammo skill runs it is the magazine instead — a yellow cell per round, emptying from the right, `b.snap ammo`, and the skill draws no standing aura, UnitInfo `ammoSkill`), 斩业星熊's 我执 pool as a red bar over her drained HP bar (`b.snap neg`), 伺夜's 狼群 count as a row of diamonds under the bars, the lit ones the 狼影 left (`b.snap wolves`; 0.2.2, GitHub PR #303 / #378 / #383), status icons, the element gauge row under the bars — icon + white bar of the remaining 元素值, element-coloured refill during a 爆发冷却 (§18.3, §20.8); enemies drawn × their official prefab factor (enemies.json `modelScale`, §20.4); an enemy's mode — the `form` of a sim fx (`shared/protocol.js fxForm`), or UnitInfo `form` for a view built later through `render/app/info.js renderInfo` — switches clip sets (`render/units.js FORMS`: 掠海漂移体 crawl, 暴鸰 bombed, 转译基底's forms, the 逐火 embers, the leaders' 重生, 守墓石像, the 孤岛风云 prisoners' warning and 解放; closing clips timed from the fx `dur`), a one-off cast (`PROJ[kind].once`) plays once, and a local-only model's web alias is tinted (`ALIAS_TINT`: 灼热 / 炽焰源石虫, §21.14); damage numbers (phys orange-white, arts purple, true white, heal green); projectiles drawn per kind, hit sparks and melee slashes, skill activation bursts and auras (§17.3); death fade for enemies, summons and devices — a knocked-out operator stays on its tile in its held Die pose under a redeploy ring (§18.3); leak flash at the objective. Fallback when Spine missing/failed: avatar in a rarity-coloured diamond with bob/lunge tweens — a failed or timed-out model is loaded again (bounded), and a view re-resolves its model and avatar when the asset manifest arrives late or the tab is shown again (§21.27). 60 fps target with ≤ 120 units (a crowd draws its Spine units through the cached atlas, `render/impostor.js`, each refreshed every k-th frame: the refresh turns follow this frame's update order — `ctx.impostorSlot()`, a counter reset at the start of every frame — so a frame refreshes ⌊n/k⌋ or ⌈n/k⌉ of n units instead of whatever random per-unit phases gave, and a unit not refreshed for 2k frames is refreshed regardless; GitHub PR #334 by @Cloudnyco, issue #327 step 1 — its step 2, a time budget by priority, is not done; a model whose pose cannot change — `SpineActor.poseHeld()`: a knocked-out operator in the held end of its Die clip, a frozen model — is neither re-posed nor redrawn, only its clock runs, until a new clip, a revive, a wind-up, a form's closing clip, a clipping switch or a zoom past the impostor's rescale threshold; an impostor's slot is sized from every clip the unit can play — measured once per skeleton, a few frames after the model loads (`render/spine.js clipBounds`, sampled within a 2.5 ms frame budget; until then the unit is drawn directly); no model shows its setup pose (one frozen before its first update — built while stunned without a stun clip — shows its clip's first frame) —, a clip too large for that box (`render/units.js impostorBox`: mostly a deploy drop from above) is drawn directly while it plays, and the atlas draws every slot clipped to its own rectangle (a scissor rect; every batch flush gets its own vertex buffer, which Apple GPUs need), so no pose is cut and nothing is left in another unit's slot (GitHub #542, §31.6); the redeploy rings draw one shared rasterised label per number, `render/textures.js downLabel`, the same pixels as a Text of their own). Picking (which unit a press / hover / dropped item is on) and the drag target follow one rule, `render/pick.js` — the tile under the pointer (§18.1); the Spine store never hands out a skeleton whose unload is in flight (§17.1).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+41
-34
@@ -107,7 +107,8 @@ Regression covers normal two-, three- and four-human browser flows, the final tu
|
||||
|
||||
### 31.6 Impostor bounds and slot clearing (#542)
|
||||
|
||||
`render/spine.js clipBounds / stepClipBounds`, `render/units.js impostorBox / _impBox / _impBypass`, `render/impostor.js`.
|
||||
`render/spine.js clipBounds / stepClipBounds / SpineActor.update`, `render/units.js impostorBox / _impBox / _impBypass`,
|
||||
`render/impostor.js`, `render/app.js`.
|
||||
|
||||
- **Seen** (GitHub #542 by @ImPQlc; quality 高, usually from round 7 on): 野鬃's flag cut, offset and leaving stray pixels
|
||||
while she idles; 浊心斯卡蒂's head cut in her S3 flight (Skill_3_Begin); 隐现's pistol muzzle cut, offset and leaving
|
||||
@@ -129,37 +130,43 @@ Regression covers normal two-, three- and four-human browser flows, the final tu
|
||||
a region by its attachment rectangle, a mesh by its vertices; slots drawn at alpha 0 skipped. It is queued when the model
|
||||
loads and sampled within 2.5 ms per frame (`stepClipBounds`, called by `render/app.js`), ≈ 9 ms per skeleton on the
|
||||
roster (53 ms at most), shared by every unit of that skeleton; a unit is drawn directly until its box is ready. (2) The
|
||||
box is the setup pose ∪ the idle clip (never smaller than the setup pose), then each other clip in the order of the area
|
||||
it adds while the box stays within 4 × that core (a looping or held clip) or 2 × (a one-off clip: the deploy clip, a
|
||||
skill's, stun's or move's begin and end, a form's change), padded by 3 % + 8 units. A clip that does not fit (`over`: 319
|
||||
of the roster's 681 deploy clips — the drop from above, up to 1,700 units tall — and the looping clips of 6 skeletons with
|
||||
screen-wide effects) is drawn directly while it plays; the unit keeps its slot (`_impBypass`). The box is computed once
|
||||
per unit: no bounds work per frame. (3) The atlas draws each slot's skeleton in a container masked by the slot rectangle,
|
||||
which PIXI draws as a scissor rect (no stencil, no render-pass break): nothing paints outside a slot, and the slot's
|
||||
eraser clears all that an earlier pose can have left in it.
|
||||
- **Cost**: slot sizes grow with the boxes. The 120-unit stress crowd at 1600 × 900 (DPR 1) uses 8.7 M px of slots instead
|
||||
of 4.4 M (2.7 instead of 1.2 atlas pages, still no private render target). On the roster the boxes add up to 1.86 × the
|
||||
old area (the median skeleton 1.39 ×; without the setup-pose floor it would be 1.23 ×). The atlas keeps its ≤ 3 + 2 pages;
|
||||
a unit that finds no slot keeps a private render target, as before. At DPR 2 a page holds a quarter of the CSS pixels:
|
||||
the same crowd at 1280 × 720 keeps 39 of its 120 units in the atlas instead of 119 (81 private render targets; 55
|
||||
instead of 60 fps in headless Chrome), at 1920 × 1080 39 instead of 58; at 844 × 390, DPR 3, all 120 stay in two pages.
|
||||
CPU: one batch flush per redrawn slot.
|
||||
`tools/perfbench.mjs` (844 × 390, DPR 2, quality medium / low, where those battles draw through the atlas; 12 runs of
|
||||
6 s): total busy JS +0.8 %, the impostor-redraw stage +14 % (+0.1–0.25 ms per frame at a 4 × CPU throttle), 60 fps in
|
||||
every run before and after.
|
||||
- Open: the setup-pose floor doubles the cost. A model shows its setup pose only when it is frozen before its first update
|
||||
(built while stunned without a stun clip, or dead without a death or idle clip). If such a model were posed at its
|
||||
clip's first frame instead, the setup pose could be dropped from the box: the stress crowd's slots would add up to
|
||||
1.16 × the old area (DPR 1), and at 1280 × 720, DPR 2, 116 of the 120 units would stay in the atlas (60 fps). Kept as
|
||||
specified for #542; the maintainer's call.
|
||||
- [ASSUMED]: the growth limits (4 × / 2 ×), the 3 % + 8 padding, the sampling rate and the 2.5 ms budget. A pose between two
|
||||
samples can still pass the padded box for a frame (33 of the 681 skeletons by a denser 60 Hz check, all in long clips
|
||||
box is the idle clip, then each other clip in the order of the area it adds while the box stays within 5 × the idle
|
||||
clip's area (a looping or held clip) or 2 × (a one-off clip: the deploy clip, a skill's, stun's or move's begin and end, a
|
||||
form's change), padded by 3 % + 8 units. A clip that does not fit (`over`: 355 of the roster's 681 deploy clips — the drop
|
||||
from above, up to 1,700 units tall — and the looping or held clips of 13 skeletons with screen-wide effects, among them
|
||||
帕拉斯's skills and 诗怀雅's attack) is drawn directly while it plays; the unit keeps its slot (`_impBypass`). The box is
|
||||
computed once per unit: no bounds work per frame. The setup pose needs no room: a model frozen before its first update —
|
||||
built while its unit is stunned and it has no stun clip (202 of the 210 operators have none), or dead without a death or
|
||||
idle clip — showed its setup pose, which no clip shows (隐现's Back model stands 900 units right of its feet in it);
|
||||
`SpineActor.update` now poses it once at its clip's first frame. (3) The atlas draws each slot's skeleton in a container
|
||||
masked by the slot rectangle, which PIXI draws as a scissor rect (no stencil, no render-pass break): nothing paints
|
||||
outside a slot, and the slot's eraser clears all that an earlier pose can have left in it. Each redrawn slot is a batch
|
||||
flush of its own, and PIXI uploaded its one batch vertex buffer again for every flush, which stalls Apple GPUs (Chrome,
|
||||
ANGLE Metal, Apple M4: the 120-unit crowd at 1920 × 1080 ran at 42–58 fps instead of 60, by the machine's load), so
|
||||
`render/app.js` turns PIXI's `BatchRenderer.canUploadSameBuffer` off: a buffer per flush, PIXI's own setting on iOS.
|
||||
Switched back and forth within one page it gave 60 fps against 42 (DPR 2) and 50 (DPR 1) with the shared buffer —
|
||||
turning it off only around the atlas pass gave 55 — for ≈ 0.3 ms more render CPU per frame; under load it also steadied
|
||||
the 0.2.5 renderer (51 → 59 fps at DPR 2).
|
||||
- **Cost**: slot sizes grow with the boxes. On the roster they add up to 1.26 × the old area (the median skeleton
|
||||
1.13 ×). The 120-unit stress crowd at 1600 × 900 (DPR 1) uses 5.5 M px of slots instead of 4.4 M (two of the three atlas
|
||||
pages, no private render target). At DPR 2 a page holds a quarter of the CSS pixels: the crowd at 1280 × 720 keeps 98–108
|
||||
of its units in the atlas instead of 118–120, at 1920 × 1080 59–61 instead of 58–59 (its private render targets 4.4 M px
|
||||
instead of 2.8 M); at 844 × 390, DPR 3, all 120 stay in two pages. 60 fps in every case, before and after (runs on a
|
||||
quiet machine). A unit that finds no slot keeps a private render target, as before; the atlas keeps its ≤ 3 + 2 pages.
|
||||
CPU: `tools/perfbench.mjs` (844 × 390, DPR 2, quality medium / low, where those battles draw through the atlas; four
|
||||
battles at a 4 × CPU throttle, two interleaved runs before and after): the view's JS 4.69 → 5.04 ms per frame, the
|
||||
impostor-redraw stage +21 % (+0.12 ms per frame), all busy JS +0.4 %, 60 fps in every run.
|
||||
- [ASSUMED]: the growth limits (5 × / 2 ×), the 3 % + 8 padding, the sampling rate and the 2.5 ms budget. A pose between two
|
||||
samples can still pass the padded box for a frame (42 of the 681 skeletons by a denser 60 Hz check, all in long clips
|
||||
sampled sparser than that); the scissor cuts it and leaves nothing behind.
|
||||
- Tests: `test/render/impostor-bounds.test.js` (synthetic Spine 3.8 skeletons: setup and clip bounds, an attachment shown
|
||||
for 0.05 s, an alpha-0 slot, the budgeted queue; `impostorBox` / `playedClips`; every atlas slot's clip box and eraser;
|
||||
UnitView: drawn directly until measured, the over-clip bypass keeping its slot, the legacy fallback, a pen figure),
|
||||
`test/render/impostor-bounds-models.test.js` (the three operators through the real runtime: every pose of every clip
|
||||
they play, at 30 Hz, inside their box; the old box cut 野鬃 by 103 units and 浊心斯卡蒂 by 188; skips without the
|
||||
assets), `test/render/impostor-bounds.browser.test.js` (`RENDER_E2E=1`: the held stress crowd — inside their slots, no
|
||||
atlas pixel outside a live slot; the scissor on a bare page; 野鬃's deploy drawn directly), `test/render/boardart.test.js`
|
||||
(the allocator's test doubles). Dev: `render-demo.html?scene=stress&ops=<chessId>,…`. Golden: unchanged.
|
||||
- Tests: `test/render/impostor-bounds.test.js` (synthetic Spine 3.8 skeletons: clip bounds, an attachment shown for
|
||||
0.05 s, an alpha-0 slot, the budgeted queue; a model frozen before its first update — stunned without a stun clip, dead
|
||||
without a death or idle clip — posed once at its clip's first frame; `impostorBox` / `playedClips`; every atlas slot's
|
||||
clip box and eraser; UnitView: drawn directly until measured, built while stunned, a skeleton without an Idle clip, the
|
||||
over-clip bypass keeping its slot, the legacy fallback, a pen figure), `test/render/impostor-bounds-models.test.js` (the
|
||||
three operators through the real runtime: every pose of every clip they play, at 30 Hz, inside their box; the old box
|
||||
cut 野鬃 by 103 units and 浊心斯卡蒂 by 188; 隐现's Back model built while stunned shows its Idle's first frame inside
|
||||
the box, its setup pose lies outside it; skips without the assets), `test/render/impostor-bounds.browser.test.js`
|
||||
(`RENDER_E2E=1`: the held stress crowd — inside their slots, no atlas pixel outside a live slot; the scissor on a bare
|
||||
page; 野鬃's deploy drawn directly), `test/render/boardart.test.js` (the allocator's test doubles). Dev:
|
||||
`render-demo.html?scene=stress&ops=<chessId>,…`. Golden: unchanged.
|
||||
Reference in new issue
Block a user