Split out of #1584 as a self-contained mitigation.
Current state
packages/melonjs/src/video/texture/cache.js:78 — when no texture unit is free:
// No units available — flush the current batch and reset assignments
if (this.renderer.currentBatcher) {
this.renderer.currentBatcher.flush();
}
this.units.clear();
this.usedUnits.clear();
...
emit(GPU_TEXTURE_CACHE_RESET);
The eviction policy is evict everything. One texture too many discards all N assignments, and the emitted event makes every batcher drop its cached bindings as well — _onTextureCacheReset() (webgl/batchers/material_batcher.js:100) zeroes boundTextures wholesale. resetUnitAssignments() (cache.js:129) is a second entry point to the same wipe.
The flush itself is unavoidable: quads already in the pending batch reference unit indices, so those vertices must be drawn before any unit is reassigned. Discarding the other N-1 assignments is not.
Proposal
Flush, then evict a single least-recently-used unit and keep the rest. The event carries which unit moved, so a batcher forgets one entry instead of its whole binding table.
|
now |
after |
| flushes per overflow |
1 |
1 (unchanged) |
| textures re-bound per overflow |
N |
1 |
| few hot textures, occasional cold ones |
thrashes |
hot ones stay resident |
| strict round-robin over N+1 textures |
thrashes |
still flushes each time, re-binds 1 |
Never worse than today, and in the common case — a handful of textures carrying most draws, plus a tail of occasional ones — it turns the cliff into a slope.
LRU is defeated by strict round-robin, which evicts precisely the texture needed next, every time. That is expected and accepted here: the flush count in that case is already one per texture and this change does not raise it, while still cutting the re-bind count. Removing the round-robin case structurally is #1584's job, not this ticket's.
Scope
texture/cache.js — per-unit last-use tracking and the evict-one path, in both allocateTextureUnit and resetUnitAssignments
system/event.ts — the reset event carries a unit index (a full reset stays expressible, for context loss)
webgl/batchers/material_batcher.js — _onTextureCacheReset(unit) drops one entry; the lit subclass override that also forgets paired normal-map units needs the same narrowing
webgpu/texture/store.js subscribes to the same event (:39) — its quad path uses segments rather than units, so confirm whether it needs the narrowed form or is unaffected
Testing
Victim selection and the resulting assignment map are pure logic — no GPU required. Given a sequence of texture requests, assert which unit is evicted and that the survivors keep their assignments. Plus:
Related
Split out of #1584 as a self-contained mitigation.
Current state
packages/melonjs/src/video/texture/cache.js:78— when no texture unit is free:The eviction policy is evict everything. One texture too many discards all N assignments, and the emitted event makes every batcher drop its cached bindings as well —
_onTextureCacheReset()(webgl/batchers/material_batcher.js:100) zeroesboundTextureswholesale.resetUnitAssignments()(cache.js:129) is a second entry point to the same wipe.The flush itself is unavoidable: quads already in the pending batch reference unit indices, so those vertices must be drawn before any unit is reassigned. Discarding the other N-1 assignments is not.
Proposal
Flush, then evict a single least-recently-used unit and keep the rest. The event carries which unit moved, so a batcher forgets one entry instead of its whole binding table.
Never worse than today, and in the common case — a handful of textures carrying most draws, plus a tail of occasional ones — it turns the cliff into a slope.
LRU is defeated by strict round-robin, which evicts precisely the texture needed next, every time. That is expected and accepted here: the flush count in that case is already one per texture and this change does not raise it, while still cutting the re-bind count. Removing the round-robin case structurally is #1584's job, not this ticket's.
Scope
texture/cache.js— per-unit last-use tracking and the evict-one path, in bothallocateTextureUnitandresetUnitAssignmentssystem/event.ts— the reset event carries a unit index (a full reset stays expressible, for context loss)webgl/batchers/material_batcher.js—_onTextureCacheReset(unit)drops one entry; the lit subclass override that also forgets paired normal-map units needs the same narrowingwebgpu/texture/store.jssubscribes to the same event (:39) — its quad path uses segments rather than units, so confirm whether it needs the narrowed form or is unaffectedTesting
Victim selection and the resulting assignment map are pure logic — no GPU required. Given a sequence of texture requests, assert which unit is evicted and that the survivors keep their assignments. Plus:
gl.readPixelsidiomRelated