Skip to content

Raise the hardcoded 16-texture batch limit to the device maximum #1585

Description

@obiot

Split out of #1584 as a self-contained mitigation.

Current state

packages/melonjs/src/video/webgl/batchers/quad_batcher.js:29

this.maxBatchTextures = Math.min(renderer.maxTextures, 16);

renderer.maxTextures is the device's MAX_TEXTURE_IMAGE_UNITS (webgl_renderer.js:149). 16 is only the WebGL 2 floor: desktop GPUs commonly report more, and most modern phones report 32. So the hardcoded 16 is discarding half the available capacity on the large majority of current hardware, mobile included — this is not a desktop-only win.

The fragment shader is generated for the count (buildMultiTextureFragment(n)), so the generator already supports any n. The 16 is a policy choice, not a structural limit.

Two things this fixes

1. The cliff moves. A scene batches until it exceeds the limit; past it, throughput collapses (see #1584 for the mechanism). Raising the limit to the device value moves that boundary from 17 textures to whatever the hardware supports.

2. It removes a capacity mismatch that currently wastes work. TextureCache is constructed with max_size = renderer.maxTextures (webgl_renderer.js:270) while the shader only addresses min(maxTextures, 16). Where the device reports more than 16 the two disagree, and every overflow does this:

  1. uploadTexturecache.getUnitallocateTextureUnit returns unit 16 — free as far as the cache is concerned — and the texture is uploaded and bound to a GL unit the shader can never read
  2. addQuad then finds unit >= this.maxBatchTextures (quad_batcher.js:287), flushes, calls resetUnitAssignments(), and re-uploads the same texture to unit 0

So a full bind — and on first sight a full texImage2D — is thrown away per overflow, and the cache's own exhaustion path never fires for quads because the batcher's wipe always precedes it. Making the two agree removes the wasted work regardless of what the limit is set to.

On a device reporting exactly 16 the min() already makes them agree and neither problem exists — but since most current hardware reports 32, today's default means the majority of devices are paying for this mismatch on every overflow.

Risks

  • Shader cost. The generated fragment shader carries one sampler and one switch arm per slot. Only one arm executes per fragment, so this is not 2× per-pixel work — the cost is shader compile time and register pressure from the extra sampler declarations. Worth measuring on a mid-range phone rather than assumed either way.
  • Older devices sitting at the WebGL 2 floor of 16 are unaffected: min() already resolves to 16 there, so nothing changes for them.
  • Driver quirks with large sampler arrays are a known hazard and want testing across backends before the default changes.

Open decision

Whether to take the device value unconditionally, or expose a maxTextures application setting so a project can raise or lower it. A setting also gives an escape hatch if a specific device misbehaves. Needs a call before implementation.

Testing

Scope

This moves the cliff, it does not remove it — a scene exceeding the new limit degrades exactly as before. #1584 tracks the structural fix; this is the cheap mitigation that is worth having either way.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions