0.2.6-dev: impostor review follow-ups (#542) — a skeleton stays in the clip-bounds queue only while an actor uses it (the last one destroyed drops its pending clips, the clip in progress and its scratch skeleton; a later actor queues them again), a clip whose sampling throws is recorded as not measurable and drawn directly while it plays instead of counting as empty, and a frozen model's first update counts as posed only when it succeeded; DESIGN §31.6 updated; golden: unchanged
This commit is contained in:
1 parent
4c2fe9431d
commit
47004a3651
4 files changed
+197
-78
No files matched your search
+65
-58
@@ -111,62 +111,69 @@ Regression covers normal two-, three- and four-human browser flows, the final tu
|
||||
`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
|
||||
stray pixels during her skill.
|
||||
- **Cause** (reproduced in headless Chrome: the dev demo's stress crowd, 120 units at impostor interval 4, the three held
|
||||
in their widest poses): a unit's impostor slot was sized once from the pose it showed when it first became an impostor,
|
||||
plus a margin (`_impBox`: the pose's bounds + 30 % / 22 %). A later pose past that box was drawn past the slot's edge:
|
||||
cut in the unit's own sprite and painted into whatever lay beside the slot on the atlas page. A page is cleared on its
|
||||
first flush only and each eraser covers its own slot, so a spill into free space stayed, and a spill into another unit's
|
||||
slot showed on that unit until it was redrawn in a frame without the spill. 浊心斯卡蒂's flight reaches y −704 (skeleton
|
||||
units) against a box top near −516 (54–57 px past her slot in 5 of 5 runs); 野鬃's idle flag reaches x −434, 103 units
|
||||
past a box sized from her setup pose (0–47 px past her slot over 5 runs: it depended on that first pose); after one clean
|
||||
redraw of every impostor the atlas pages held 0–1,405 pixels outside every live slot. 隐现's own poses fit her box (the
|
||||
silencer reaches x 197, her box 225): her muzzle sits at her slot's edge, where a neighbour's spill lands. Of the
|
||||
roster's 681 skeletons, 598 play a clip that leaves a box sized that way from their idle's first frame. No clipping
|
||||
attachment is involved (`spine.js _eyeMaskFallback`, #177, unchanged).
|
||||
while she idles; 浊心斯卡蒂's head cut in her S3 flight (Skill_3_Begin); 隐现's pistol muzzle cut, offset and leaving stray
|
||||
pixels during her skill.
|
||||
- **Cause** (reproduced in headless Chrome: the dev demo's stress crowd, 120 units at impostor interval 4, the three
|
||||
held in their widest poses): a unit's impostor slot was sized once from the pose it showed when it first became an
|
||||
impostor, plus a margin (`_impBox`: the pose's bounds + 30 % / 22 %). A later pose past that box was drawn past the
|
||||
slot's edge: cut in the unit's own sprite and painted into whatever lay beside the slot on the atlas page. A page is
|
||||
cleared on its first flush only and each eraser covers its own slot, so a spill into free space stayed, and a spill
|
||||
into another unit's slot showed on that unit until it was redrawn in a frame without the spill. 浊心斯卡蒂's flight reaches
|
||||
y −704 (skeleton units) against a box top near −516 (54–57 px past her slot in 5 of 5 runs); 野鬃's idle flag reaches x
|
||||
−434, 103 units past a box sized from her setup pose (0–47 px past her slot over 5 runs: it depended on that first
|
||||
pose); after one clean redraw of every impostor the atlas pages held 0–1,405 pixels outside every live slot. 隐现's own
|
||||
poses fit her box (the silencer reaches x 197, her box 225): her muzzle sits at her slot's edge, where a neighbour's
|
||||
spill lands. Of the roster's 681 skeletons, 598 play a clip that leaves a box sized that way from their idle's first
|
||||
frame. No clipping attachment is involved (`spine.js _eyeMaskFallback`, #177, unchanged).
|
||||
- **Now**: (1) every clip a unit can play (its manifest roles with every skill's set and the Run cycle, its `FORMS` clip
|
||||
sets and change clips) is measured once per skeleton data on a scratch skeleton: 60 poses a second, at most 49 per clip;
|
||||
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 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: 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.
|
||||
sets and change clips) is measured once per skeleton data on a scratch skeleton: 60 poses a second, at most 49 per
|
||||
clip; 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.
|
||||
A skeleton stays queued only while an actor uses it: when the last one is destroyed (a model swap, a battlefield torn
|
||||
down) its pending clips and its scratch skeleton are dropped, so nothing holds a skeleton the Spine store let go or is
|
||||
measured ahead of the next scene's models, and a later actor queues again what was never measured. (2) The 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`). So is a
|
||||
clip whose sampling threw (recorded as not measurable, never as drawing nothing — the scissor would cut it). 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 (again on the next frame if that update
|
||||
threw). (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: clip bounds, an attachment shown for 0.05
|
||||
s, an alpha-0 slot, the budgeted queue, a skeleton leaving the queue with its last actor, a clip whose sampling
|
||||
throws; 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, again after an update that threw; `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, an unmeasurable attack or idle clip drawn directly, 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