Report zero queued alpha for an entry the yield pass hid

getQueuedOverlayEntryAlpha now returns 0 for a yielding entry that
declutterYieldingEntries marked collisionHidden this frame. That pass runs
before custom lanes, so they no longer read full opacity for an entry that
will not paint.
This commit is contained in:
German Khokhlov committed 2026-11-10 14:06:24 +04:00
1 parent 8eb0173940
commit 20d58cf888
2 files changed
+42 -3

No files matched your search

+5 -3
View File
@@ -1047,9 +1047,9 @@ export function getOverlayPaintRect(sourceId, entryId) {
}
/**
* Opacity an entry paints with this frame, or 0 when it is not queued. Custom
* lanes may call it before later lanes publish paint rects. Leaves out the
* Cyber sonar fade and yield-collision hiding.
* Opacity an entry paints with this frame, or 0 when it is not queued or the
* yield-collision pass hid it. Custom lanes may call it before later lanes
* publish paint rects. Leaves out the Cyber sonar fade.
* @param {string} sourceId
* @param {string} entryId
* @returns {number}
@@ -1059,6 +1059,8 @@ export function getQueuedOverlayEntryAlpha(sourceId, entryId) {
const item = _queuedItemBySource.get(sourceId)?.get(entryId);
// Pooled items are reassigned every frame: check the frame and the entry.
if (!item || item.stamp !== _frameStamp) return 0;
// Declutter has already run when custom lanes call this.
if (item.collisionHidden) return 0;
const record = item.record;
if (record.entry.id !== entryId || record.entry.source !== sourceId) return 0;
return (
+37
View File
@@ -1259,6 +1259,43 @@ test('a detection lane sees which cards this frame will paint before they publis
env.cleanup();
});
test('a detection lane reads zero for a yielding entry the declutter pass hid this frame', () => {
// Declutter runs before custom lanes, so a yielding entry it hid must not
// report its full queued alpha to them.
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1 });
initWorldOverlay(env.viewer);
const seen = [];
registerWorldOverlayPaintLane('detection', () => {
seen.push({
winner: getQueuedOverlayEntryAlpha('cards', 'ABOVE'),
yielder: getQueuedOverlayEntryAlpha('labels', 'YIELDER'),
});
}, { id: 'detection', active: true, target: 'detection' });
// Two cards take both vertical slots, so a vertical-only yielder has none.
const at = positionAtScreen(200, 150);
const card = (id, placement) => selectedEntry(id, {
position: at,
placement,
verticalOnly: true,
collisionGroup: 'shared-card',
});
setOverlayEntries('cards', [card('ABOVE', 'above'), card('BELOW', 'below')]);
setOverlayEntries('labels', [selectedEntry('YIELDER', {
position: at,
verticalOnly: true,
collisionGroup: 'yielding-label',
yieldToCollisionGroup: 'shared-card',
})]);
env.postRender.raise();
assert.ok(getOverlayPaintRect('cards', 'ABOVE'), 'the winner painted');
assert.equal(getOverlayPaintRect('labels', 'YIELDER'), null, 'the yielder was hidden');
assert.ok(seen[0].winner > 0, 'the winner reads its queued alpha');
assert.equal(seen[0].yielder, 0, 'the yielder hidden this frame reads zero');
destroyWorldOverlay();
env.cleanup();
});
test('custom-lane painter state is bracketed by host save/restore', () => {
// A painter that leaks a clip or alpha must not corrupt later frames: the
// host wraps every custom-lane painter in save/try/finally-restore. Pin the