Skip to content

Add multi-texture batching for WebGL renderer (#1376)#1389

Merged
obiot merged 12 commits intomasterfrom
feat/multi-texture-batching-v2
Apr 16, 2026
Merged

Add multi-texture batching for WebGL renderer (#1376)#1389
obiot merged 12 commits intomasterfrom
feat/multi-texture-batching-v2

Conversation

@obiot
Copy link
Copy Markdown
Member

@obiot obiot commented Apr 15, 2026

Summary

Multi-texture batching allows up to 16 textures to be drawn in a single batch/draw call, eliminating flushes on texture changes.

Changes

  • New: shaders/multitexture.js — dynamically generates vertex/fragment shaders with per-texture sampler uniforms and if/else selection chain
  • buffer/vertex.js — new pushTextured(x, y, u, v, tint, textureId) method (6 floats per vertex)
  • material_batcher.js — new uploadTextureMulti/bindTexture2DMulti non-flushing methods
  • quad_batcher.js — uses generated multi-texture shaders, embeds texture unit index in vertex data, falls back to single-texture when custom ShaderEffect is active

Backward compatible

  • drawImage() API unchanged
  • ShaderEffect/GLShader — when active, single-texture fallback kicks in automatically
  • SpineBatcher, MeshBatcher, PrimitiveBatcher — unaffected
  • quad.vert/quad.frag — unchanged (used by ShaderEffect fallback)

Test plan

  • All 2482 tests pass
  • Build succeeds
  • Visual verification with multi-texture examples

Closes #1376

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 15, 2026 11:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates melonJS’ WebGL initialization defaults and improves robustness around compressed texture format detection, along with an example update and a package version/changelog bump.

Changes:

  • Guard WebGLRenderer.getSupportedCompressedTextureFormats() against missing GL context.
  • Change default failIfMajorPerformanceCaveat to false in CanvasRenderTarget attributes.
  • Bump melonjs package version to 19.1.0 and add 19.1.0 changelog entries; update the compressed textures example to the setText() + preDraw/draw/postDraw pattern.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/melonjs/src/video/webgl/webgl_renderer.js Adds a null/undefined GL context guard for compressed texture extension probing.
packages/melonjs/src/video/rendertarget/canvasrendertarget.js Changes default WebGL context creation attribute failIfMajorPerformanceCaveat.
packages/melonjs/package.json Version bump to 19.1.0.
packages/melonjs/CHANGELOG.md Adds 19.1.0 entry describing the WebGL changes and example fix.
packages/examples/src/examples/compressedTextures/ExampleCompressedTextures.tsx Updates text rendering to explicit setText() + renderable draw lifecycle calls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/examples/src/examples/compressedTextures/ExampleCompressedTextures.tsx Outdated
Comment thread packages/melonjs/src/video/webgl/webgl_renderer.js
Comment thread packages/melonjs/src/video/rendertarget/canvasrendertarget.js Outdated
Comment thread packages/melonjs/CHANGELOG.md Outdated
Comment thread packages/melonjs/CHANGELOG.md
Copilot AI review requested due to automatic review settings April 15, 2026 23:58
@obiot obiot force-pushed the feat/multi-texture-batching-v2 branch from 7ac7b2c to 8b98be3 Compare April 15, 2026 23:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/melonjs/src/video/webgl/batchers/quad_batcher.js
Comment thread packages/melonjs/src/video/webgl/batchers/material_batcher.js
@obiot obiot force-pushed the feat/multi-texture-batching-v2 branch from 8b98be3 to f151ef4 Compare April 16, 2026 00:09
Copilot AI review requested due to automatic review settings April 16, 2026 00:15
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/melonjs/src/video/webgl/batchers/quad_batcher.js
Comment thread packages/melonjs/src/video/webgl/batchers/material_batcher.js
Comment thread packages/melonjs/src/video/webgl/webgl_renderer.js
Copilot AI review requested due to automatic review settings April 16, 2026 02:44
@obiot obiot force-pushed the feat/multi-texture-batching-v2 branch from e7be282 to 3647800 Compare April 16, 2026 02:44
obiot and others added 3 commits April 16, 2026 10:48
Allows up to 16 textures in a single draw call, eliminating GPU flushes
on texture changes. ~80% fewer draw calls on the platformer example,
1000 quads/flush on the sprite benchmark with 5000 objects.

- Generate multi-texture fragment shader dynamically with per-texture
  sampler uniforms and if/else selection chain
- Static quad-multi.vert with aTextureId attribute
- push() accepts optional textureId parameter (6 floats when provided)
- uploadTexture/bindTexture2D accept flush parameter to skip flushing
- QuadBatcher falls back to single-texture when custom ShaderEffect active
- Track scissor state with _scissorActive instead of gl.isEnabled() query
- Add unit tests for push with textureId and fragment shader generation
- Add "Tap or Click to spawn" hint to benchmark example

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Flush and reset texture cache if assigned unit >= maxBatchTextures
  (prevents wrong texture sampling on GPUs with >16 units)
- Guard hasSupportedCompressedFormats against null format entries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Texture re-uploads (e.g. Text canvas changes) in multi-texture mode
no longer force a batch flush. The flush flag is now passed from
uploadTexture → createTexture2D → bindTexture2D consistently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@obiot obiot force-pushed the feat/multi-texture-batching-v2 branch from 3647800 to b553d61 Compare April 16, 2026 02:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/melonjs/src/video/webgl/buffer/vertex.js Outdated
obiot and others added 2 commits April 16, 2026 10:51
Flush before and after drawing gradient fillRect — the shared gradient
canvas is reused across gradients, so the texture must be uploaded
immediately before the canvas is overwritten by the next gradient.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a canvas texture is invalidated (Text, Gradient, etc.), flush
pending draws that reference the old texture data before unbinding.
This is the single correct fix point for all dynamic canvas textures.
Simplifies the gradient fillRect path — no longer needs manual flushes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 16, 2026 02:55
@obiot obiot force-pushed the feat/multi-texture-batching-v2 branch 2 times, most recently from ec60e38 to c2316dc Compare April 16, 2026 03:00
Replace video.init() + game/state globals with new Application().
Pass app to CompressedTextureDisplay for viewport access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@obiot obiot force-pushed the feat/multi-texture-batching-v2 branch from c2316dc to 1fda0ca Compare April 16, 2026 03:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/melonjs/src/video/rendertarget/canvasrendertarget.js Outdated
Comment thread packages/melonjs/tests/vertexBuffer.spec.js Outdated
Comment thread packages/melonjs/src/video/webgl/buffer/vertex.js Outdated
Comment thread packages/melonjs/src/video/webgl/batchers/quad_batcher.js
Comment thread packages/melonjs/src/video/webgl/webgl_renderer.js
- Application default now false — allows WebGL on machines with
  blocklisted GPU drivers, matching PixiJS and Phaser
- Non-WebGL tests switched to video.CANVAS for deterministic behavior
- WebGL-specific tests (webgl_save_restore, texture) keep video.AUTO
  with explicit failIfMajorPerformanceCaveat: true

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 16, 2026 03:15
@obiot obiot force-pushed the feat/multi-texture-batching-v2 branch 4 times, most recently from c630485 to 485e6e6 Compare April 16, 2026 03:20
…king

- push(): use vertexSize > 5 instead of arguments.length (avoids V8 deopt),
  always write default textureId 0 when vertexSize is 6
- TextureCache.resetUnitAssignments(): encapsulate unit reset logic
- clipRect: use _scissorActive instead of gl.isEnabled() query
- Update vertex buffer tests for new push behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@obiot obiot force-pushed the feat/multi-texture-batching-v2 branch from 485e6e6 to aa4e87e Compare April 16, 2026 03:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/melonjs/src/video/texture/cache.js
Comment thread packages/melonjs/src/video/webgl/batchers/quad_batcher.js
Comment thread packages/melonjs/src/application/defaultApplicationSettings.ts Outdated
Comment thread packages/melonjs/CHANGELOG.md Outdated
obiot and others added 2 commits April 16, 2026 11:22
- resetUnitAssignments() now clears batcher's boundTextures and
  currentTextureUnit to prevent stale GL textures after reset
- CHANGELOG reflects actual failIfMajorPerformanceCaveat default (false)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Canvas performs better than software WebGL — keep true as the default
so AUTO falls back to Canvas on blocklisted GPUs. The gl guard in
getSupportedCompressedTextureFormats handles the crash case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 16, 2026 03:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/melonjs/src/video/webgl/shaders/multitexture.js
Comment thread packages/melonjs/src/video/rendertarget/canvasrendertarget.js
@obiot obiot force-pushed the feat/multi-texture-batching-v2 branch 2 times, most recently from 191b992 to 4bd0052 Compare April 16, 2026 03:34
Copilot AI review requested due to automatic review settings April 16, 2026 03:34
…Draw

Text.draw(renderer, text, x, y) standalone pattern was removed in 19.0.
Use pos.set + setText + preDraw/draw/postDraw instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@obiot obiot force-pushed the feat/multi-texture-batching-v2 branch from 4bd0052 to 26c594e Compare April 16, 2026 03:35
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 243 to +249
hasSupportedCompressedFormats(format) {
const supportedFormats = this.getSupportedCompressedTextureFormats();
for (const supportedFormat in supportedFormats) {
for (const extension in supportedFormats[supportedFormat]) {
if (format === supportedFormats[supportedFormat][extension]) {
const entry = supportedFormats[supportedFormat];
if (entry === null || typeof entry === "undefined") {
continue;
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renderer.hasSupportedCompressedFormats() is now null-safe, but WebGLRenderer still overrides this method with the previous implementation (which will still throw when an unsupported extension family is null). This means WebGL compressed texture loading can still crash (e.g. parseCompressedImage() calls _renderer.hasSupportedCompressedFormats). Consider removing the WebGLRenderer.hasSupportedCompressedFormats override so it inherits this fixed base implementation, or apply the same null/undefined guard in the override.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +16
export function buildMultiTextureFragment(maxTextures) {
const count = Math.max(maxTextures, 1);
const lines = [];

// declare sampler uniforms
for (let i = 0; i < count; i++) {
lines.push("uniform sampler2D uSampler" + i + ";");
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildMultiTextureFragment() produces invalid GLSL when maxTextures <= 0 (it still emits a fallback sampling uSampler0 even though no sampler uniforms were declared). Adding an explicit validation/throw (or clamping to at least 1) would prevent hard-to-diagnose shader compile failures if an unexpected value is passed in.

Copilot uses AI. Check for mistakes.
this.bufferF32[offset + 3] = v;
this.bufferU32[offset + 4] = tint;
if (this.vertexSize > 5) {
this.bufferF32[offset + 5] = textureId || 0;
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

textureId || 0 relies on truthiness and will silently coerce some invalid numeric values (e.g. NaN) to 0. Using a nullish check (textureId ?? 0) would be more explicit about the intent (default only when omitted) and avoids masking unexpected inputs.

Suggested change
this.bufferF32[offset + 5] = textureId || 0;
this.bufferF32[offset + 5] = textureId ?? 0;

Copilot uses AI. Check for mistakes.
Comment on lines +126 to +129
// re-bind sampler uniforms after context restore
for (let i = 0; i < this.maxBatchTextures; i++) {
this.defaultShader.setUniform("uSampler" + i, i);
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QuadBatcher.reset() is called on normal renderer resets (e.g. GAME_RESET), not on WebGL context restore (where batcher.init() is invoked). The comment about “after context restore” is misleading, and the uniform rebind here may be redundant; either adjust the comment or move/restrict the rebind logic to the actual context-restore path.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +6
### Added
- WebGL: multi-texture batching — up to 16 textures (based on device capabilities) drawn in a single batch/draw call, eliminating GPU flushes on texture changes. Automatically falls back to single-texture mode when a custom `ShaderEffect` is active. ~80% fewer draw calls on the platformer example (14 vs ~70 flushes/frame), with an estimated 30-50% FPS improvement on low-end mobile devices.
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changelog entry doesn’t mention that CanvasRenderTarget’s internal default failIfMajorPerformanceCaveat behavior changed in this release (see canvasrendertarget.js). If that default change is intended, it should be documented here; otherwise consider reverting the default to avoid an untracked behavior change.

Copilot uses AI. Check for mistakes.
scaleMethod: "flex",
renderer: video.WEBGL,
});

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code assumes app.renderer is a WebGLRenderer, but autoDetectRenderer() can fall back to CanvasRenderer when WebGL isn’t available or is rejected due to failIfMajorPerformanceCaveat. Consider guarding with renderer.type.includes("WebGL") / instanceof WebGLRenderer and showing a user-facing message (or forcing WebGL) to avoid incorrect behavior/casts in the compressed-texture example.

Suggested change
if (!app.renderer.type.includes("WebGL")) {
app.world.addChild(new ColorLayer("background", "#0f172a"), 0);
app.world.addChild(
new Text(24, 24, {
text: "Compressed textures require WebGL.\nWebGL is not available in this browser/environment.",
size: 24,
fillStyle: "#e2e8f0",
}),
1,
);
return;
}

Copilot uses AI. Check for mistakes.
Comment on lines 97 to +105
this.off_icon_region = tex.getRegion(offIcon);
this.setOpacity(0.5);
this.isSelected = true;
this.label_on = onLabel;
this.label_off = offLabel;
this.floating = false;

this.font = new Text(0, 0, {
// create label as a sibling — added to the parent by the caller
this.label = new Text(x + this.width, y + this.height / 2, {
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkbox label is initialized with text: onLabel, which makes the UI start in the “ON” state visually. If the intended default is “OFF” (as in the prior version that initially displayed offLabel), initialize the label with offLabel and/or call setSelected(false) in the constructor to keep region/text/state consistent from the first frame.

Copilot uses AI. Check for mistakes.
@obiot obiot merged commit d0573f8 into master Apr 16, 2026
6 checks passed
@obiot obiot deleted the feat/multi-texture-batching-v2 branch April 16, 2026 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-texture batching for WebGL renderer

2 participants