feat(tilemap): Uint16Array layerData + drawTileRaw renderer bypass (#1401 foundation)#1445
Merged
Conversation
…1401) Foundation for the upcoming GPU tilemap shader path. Refactors TMX tile layer storage and rendering so that: - TMXLayer.layerData is now a flat Uint16Array (GID + flip mask per cell). Stable Tile identity is preserved via a lazy cachedTile view-cache that allocates on the first user-facing cellAt/getTile call — games that never query tiles by coord keep it null for the layer's lifetime. - Map parsing decodes TMX layer data straight into bytes — zero Tile allocations during parse (saves ~500k constructor calls on a dense 1000x1000 map). - The orientation renderers (Orthogonal / Oblique / Isometric / Hexagonal) read layerData directly and dispatch via new drawTileRaw methods on each renderer and on TMXTileset. The per-frame render loop never constructs a Tile, never touches cachedTile. - TMXLayer.dataVersion counter bumps on setTile/clearTile — groundwork for GPU upload invalidation in the shader path. - New buildFlipTransform(matrix, flipMask, w, h) helper shared between Tile.setTileTransform and the new raw render path. - Public API (getTile, setTile, cellAt, clearTile, getTileId, getTileById, getRenderable) is unchanged. Memory: per-layer drops ~25x for games that don't query tiles by coord (~40 KB vs ~1 MB on a 100x100 dense layer). Per-frame FPS: modest gain on Canvas (~2-5% in tile-heavy scenes); the big WebGL win lands when the shader path comes online on top of this foundation. Tests: tmxlayer-data.spec.js (68 adversarial encoding tests including all 8 flip combinations + real-fixture snapshot) + tmxlayer-drawraw.spec.js (16 parity tests between legacy drawTile and new drawTileRaw). 3021/3021 suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ps (#1401) Visible tile layers now render as a single quad whose fragment shader walks the per-layer GID index texture and samples the tileset atlas. Supports animated tiles, flip bits (H/V/AD), per-layer opacity/tint/ blend mode, and oversized bottom-aligned tiles. Falls back to the legacy CPU renderer on isometric/staggered/hexagonal layers, collection-of-image tilesets, non-zero tileoffset, or non-WebGL-2 contexts. Application emits a one-shot warning when `gpuTilemap` is enabled but the renderer can't honor it. Supporting changes: - WebGL: custom GLSL ES 3.00 shaders supported through `GLShader` (precision injector + attribute extractor handle both 1.00 and 3.00). `ShaderEffect` is still 1.00-only since it pairs the user's fragment with the built-in 1.00 quad vertex shader. - New `TextureResource` / `BufferTextureResource` for synthesized (raw-buffer) textures flowing through the standard `TextureCache` + batcher path. Used by the GPU TMX renderer for the per-layer index texture and per-tileset animation lookup. - Uniform value caching across every shader the engine builds — redundant `gl.uniform*` calls are now skipped. Vec/mat values compare element-wise so scratch-buffer mutation is detected. - TMX fragment-shader fast path on `uOverflow == (0, 0)` (the common case) skips the worst-case 25-iteration candidate-cell loop. Fixes & related cleanups: - `MaterialBatcher.uploadTexture` was using its `w`/`h` parameters (destination quad size) for the POT check, causing `gl.generateMipmap` to fire on NPOT atlases under WebGL 1 (`GL_INVALID_OPERATION`) and silent wasted work under WebGL 2. Texture dimensions are now derived from the source. - Removed the unconditional `[Texture] ... is not a POT texture` warning; replaced with a scoped warning when `repeat: "repeat*"` is requested on an NPOT texture under WebGL 1 (the one case where intent is silently downgraded). - `setPrecision` correctly skips injection when the user already declares precision after `#version`, and handles single-line shaders without a trailing newline. - Attribute extractor accepts precision qualifiers between `attribute`/`in` and the type (e.g. `attribute highp vec3 foo`). - `Renderer.drawTileLayer` cached-canvas blit clamps the source rect to `layer.width - rect.pos.x` so a scrolled camera doesn't read past the canvas. - `BufferTextureResource.upload` throws a clear error when format `"rgba8ui"` is used on a WebGL 1 context. Rough perf win on a mid-tier mobile GPU (3-layer 800×600 game): ~1.5–3.5 ms reclaimed per frame; dense large maps should see ~5–8× on the rendering portion. New tests cover flip math, uniform caching, texture-resource lifecycle, and shader-path eligibility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeQL flagged the new GLSL 3.00-aware attribute regex as a polynomial-time ReDoS risk: `\s` matches newlines, and the `(?:\w+\s+)+` repetition combined with `(?:^|\n)\s*` made the engine backtrack quadratically when fed shader sources with many consecutive blank lines. Switch all interior whitespace classes to `[ \t]+` (horizontal only). Declarations live on one line in practice (the engine's own minifier removes redundant newlines anyway), and the `(?:^|\n)` line anchor remains, so behavior is unchanged on every real shader in the engine and the regex is now ambiguity-free. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `cellAt(x, y, false)`: out-of-range coord reads return `undefined` from the typed array, which fell through the `gid === 0` check and tried to resolve an invalid GID. Use `!gid` so both empty cells and out-of-range reads short-circuit to `null`. - `setLayerData`: emit a one-shot `console.warn` when an incoming TMX layer contains a (flip-stripped) GID greater than 0xFFFF — the `Uint16Array` would otherwise silently truncate it and render the wrong tile. - `setTile`: same one-shot warning for runtime mutations with an oversized tile id. - `orthogonal.js` constructor comment: stop claiming the shader uses `usampler2D` + integer reads; document that we use `sampler2D` + `texelFetch` + float decode (the actual current behavior, chosen to avoid clashing with the engine's multi-texture batching cache). - `World.gpuTilemap` doc: expand the feature list to match the shader path's real capabilities (animations, flip bits, opacity/ tint/blend, bottom-aligned overflow up to 4 cells) and tighten the fallback list (non-zero `tileoffset`, overflow > 4 cells). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `MaterialBatcher.uploadTexture`: prefer `source.videoWidth` / `source.videoHeight` as a fallback when `width`/`height` are 0. `HTMLVideoElement` exposes its actual pixel dimensions through the video-specific properties, so the POT/repeat logic was feeding `0` into `isPowerOfTwo` for unsized video sources. - `WebGLRenderer.reset()`: only drop `_orthogonalTMXGPURenderer` when the GL context is no longer valid. On a regular `GAME_RESET` (level transition with the context still live) we want to keep the cached `GLShader` so its compiled `WebGLProgram` survives, rather than leaking it and re-paying the link cost every reset. Per-layer textures are still freed unconditionally via the renderer's own `reset()`. - `OrthogonalTMXLayerGPURenderer.reset()`: when no batcher is active (context tear-down), the fallback path called `cache.delete(resource)`, but that only clears the image→atlas map and leaves the texture-unit assignment in `cache.units`. Call `cache.freeTextureUnit(resource)` alongside it so unit slots aren't held forever. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 12, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First commit toward #1401 — GPU-accelerated tilemap rendering. Refactors the TMX tile-layer data layout and renderer hot path so the upcoming WebGL2 shader path has a contiguous, GPU-uploadable backing store to draw from. No GPU shader code yet — that lands on top of this foundation in a follow-up PR.
What changed
TMXLayer.layerDatais now a flatUint16Array(GID + 3-bit flip mask per cell, row-major). Stable user-facingTileidentity is preserved via a lazycachedTileview-cache that allocates on firstcellAt/getTilecall — games that never query tiles by coord keep itnullfor the layer's lifetime.Tileallocations during load.layerDatadirectly and dispatch via newdrawTileRawmethods on each renderer and onTMXTileset. The per-frame render loop never constructs aTile.TMXLayer.dataVersioncounter bumps onsetTile/clearTile— groundwork for GPU upload invalidation in the shader path.buildFlipTransform(matrix, flipMask, w, h)helper shared betweenTile.setTileTransformand the new raw render path.Public API
Unchanged —
getTile,setTile,cellAt,clearTile,getTileId,getTileById,getRenderableall work as before.Wins
Tileconstructor calls saved on a dense 1000×1000 map.Tileallocations during render. Modest FPS gain on Canvas (~2–5% in tile-heavy scenes); WebGL gains come with the shader path.Tests
tmxlayer-data.spec.js(68 adversarial encoding tests) — all 8 flip combinations, GID range edge cases, identity stability via lazy cache, bounds validation,dataVersionmonotonicity, parser-path no-allocation guard, cross-cell isolation, real-fixture round-trip.tmxlayer-drawraw.spec.js(16 parity tests) — pixel-level byte-for-byte parity between legacydrawTileand newdrawTileRawfor every flip combination; spy-verified zeroTileconstruction during render;buildFlipTransformmatchesTile.setTileTransform.Next
Phase 2 of #1401:
WebGLRenderer.drawTileLayershader path on top of this foundation — single quad per tileset, fragment-shader tile lookup viaRG16UIindex texture upload.Test plan
pnpm -F melonjs build)🤖 Generated with Claude Code