Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- **`Camera2d` default near/far widened from `±1000` to `±1e6`** so `depth` participating in clip-space no longer cull-clips sprites with `Container.autoDepth` enabled or Y-sort patterns on tall maps. Override per-camera for tighter z bounds.

### Fixed
- **GPU TMX layer reset crash when a non-material batcher was active** (#1471). `OrthogonalTMXLayerGPURenderer.reset()` grabbed `renderer.currentBatcher` and called `deleteTexture2D` on it, but uploads always flow through the "quad" batcher (`_drawLayer`). When the previous frame left a `PrimitiveBatcher` active — e.g. the debug plugin's quadtree overlay — the reset path hit a method that doesn't exist on it and threw `TypeError: batcher.deleteTexture2D is not a function` on every stage change. Reset now pins to `batchers.get("quad")` and falls through to a manual cache cleanup if a user-supplied custom batcher doesn't expose `deleteTexture2D`.
- **WebGL `TextureCache` cross-batcher binding desync** — a unit-pool reset only cleared the current batcher's `boundTextures` map, leaving stale entries on every other batcher; meshes rendered as black silhouettes and bullets as pure white in mixed-batcher scenes after sustained gameplay. Fixed via the new `event.GPU_TEXTURE_CACHE_RESET` event consumed by `MaterialBatcher`.
- **WebGL color-attribute NaN canonicalization on Apple Metal / ANGLE** — `MeshBatcher` color attribute switched from `UNSIGNED_BYTE × 4 normalized` to `FLOAT × 4`; `Batcher.flush` / `QuadBatcher.flush` upload packed-color bytes via `vertex.toUint8()` instead of `vertex.toFloat32()` so they survive driver canonicalization. No behavior change on drivers that don't canonicalize.
- **`Stage.reset` re-applies the chosen camera's `defaultSortOn` on every reset** — covers both the loader-pinned-Camera2d → user-stage handoff and the explicit-camera (`Stage({ cameras: [new Camera3d(...)] })`) pattern. Previously distant meshes painted on top of nearer ones under perspective on a Camera3d app coming out of the default loader.
Expand Down
22 changes: 15 additions & 7 deletions packages/melonjs/src/video/webgl/renderers/tmxlayer/orthogonal.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,24 @@ export default class OrthogonalTMXLayerGPURenderer {
* @ignore
*/
reset() {
const batcher = this.renderer.currentBatcher;
// Uploads happen via the quad batcher (see `_drawLayer`), so pin
// the cleanup to that same batcher rather than whatever happens
// to be `currentBatcher` at reset time. `currentBatcher` may
// legitimately be a `PrimitiveBatcher` (e.g. debug-plugin drew
// its quadtree overlay on the previous frame) which has no
// `deleteTexture2D` and would crash the reset. Issue #1471.
const batcher = this.renderer.batchers.get("quad");
const cache = this.renderer.cache;
const drop = (resource) => {
// route through the batcher so its `boundTextures` bookkeeping
// stays in sync. When no batcher is active (e.g. context tear-
// down) we don't have a clean GL deletion path, but we still
// need to free the unit assignment — `cache.delete()` only
// touches the image→atlas map and would leave the unit slot
// held forever otherwise, so call `freeTextureUnit()` too.
if (batcher !== undefined) {
// stays in sync. When no batcher is available (context tear-
// down) or the registered "quad" batcher is a user-supplied
// custom class that doesn't extend `MaterialBatcher`, we
// don't have a clean GL deletion path — still need to free
// the unit assignment, since `cache.delete()` only touches
// the image→atlas map and would leave the unit slot held
// forever otherwise.
if (typeof batcher?.deleteTexture2D === "function") {
batcher.deleteTexture2D(resource);
} else {
cache.freeTextureUnit(resource);
Expand Down
36 changes: 36 additions & 0 deletions packages/melonjs/tests/tmxlayer-shader.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,42 @@ describe("TMXLayer shader path", () => {
expect(gpu.resources.size).toBe(0);
});

/**
* Regression for #1471: `reset()` used to grab
* `renderer.currentBatcher`, but uploads always go through the quad
* batcher (see `_drawLayer`). When the previous-frame draw left
* `currentBatcher` as a `PrimitiveBatcher` (e.g. the debug plugin's
* quadtree overlay), the reset path tried to call
* `deleteTexture2D` on a batcher that doesn't expose it — crash on
* any stage change. `reset()` now pins to `batchers.get("quad")`.
*/
it("survives reset() even when currentBatcher is not a MaterialBatcher", (ctx) => {
requireWebGL2(ctx);
const gpu = renderer._getTMXGPURendererFor("orthogonal");
const quad = renderer.setBatcher("quad");

const layer = {
cols: 2,
rows: 2,
layerData: new Uint16Array(2 * 2 * 2),
dataVersion: 0,
};
quad.uploadTexture(gpu._getResource(layer), layer.cols, layer.rows);
expect(gpu.resources.size).toBe(1);

// Now mimic the debug-plugin quadtree overlay leaving the
// primitive batcher as `currentBatcher` before the level reset.
renderer.setBatcher("primitive");
expect(renderer.currentBatcher).not.toBe(quad);

// Pre-fix this throws `TypeError: batcher.deleteTexture2D is not
// a function` and the resource map is never cleared.
expect(() => {
gpu.reset();
}).not.toThrow();
expect(gpu.resources.size).toBe(0);
});

/**
* Animation lookup: non-animated tilesets don't allocate a lookup
* entry at all (saves a texture unit + a GL texture); animated
Expand Down
Loading