Skip to content

fix(core): read texture dimensions from the plain object image form - #10539

Open
mhayk wants to merge 1 commit into
visgl:masterfrom
mhayk:fix/bitmap-layer-plain-object-image-10371
Open

fix(core): read texture dimensions from the plain object image form#10539
mhayk wants to merge 1 commit into
visgl:masterfrom
mhayk:fix/bitmap-layer-plain-object-image-10371

Conversation

@mhayk

@mhayk mhayk commented Aug 7, 2026

Copy link
Copy Markdown

Goal

Fixes #10371 — the documented plain object image form renders as opaque black, painting black over everything beneath the layer's bounds.

BitmapLayer's docs list {data: <Uint8Array>, width, height} as a supported image value (bitmap-layer.md), but in 9.x it does not render.

Root cause

createTexture() in modules/core/src/utils/texture.ts reads the dimensions off image.data:

const {width, height} = image.data;

Browser image objects (ImageData, ImageBitmap, canvas, …) hit the branch above that wraps them as image = {data: browserImage}, so image.data.width/height exist and everything works. The plain object form has constructor.name === 'Object', so it is not wrapped — it stays {data, width, height}, where data is the raw typed array. Both dimensions come back undefined, so:

device.getMipLevelCount(undefined, undefined)
// -> 1 + Math.floor(Math.log2(Math.max(undefined, undefined, 1)))  ->  NaN

The texture is created with mipLevels: NaN. Combined with the default mipmapFilter: 'linear' in DEFAULT_TEXTURE_PARAMETERS, the texture is incomplete and samples as (0, 0, 0, 1).

This runs through the shared image prop type in modules/core/src/lifecycle/prop-types.ts, so it affects every image-typed prop (BitmapLayer.image, IconLayer.iconAtlas, TextLayer, …), not just BitmapLayer.

Changes

modules/core/src/utils/texture.ts — read the dimensions from whichever level actually carries them:

const {width, height} = image.data.width === undefined ? image : image.data;

One-line behavioral change; the browser-object path is untouched.

Validation

New test/modules/core/utils/texture.spec.ts covering both forms, asserting width, height, and a valid mipLevels. Confirmed it actually catches the bug — with the fix reverted:

AssertionError: derives a valid mip level count: expected NaN to be 7

Also verified end-to-end by pixel readback, rendering a solid-yellow bitmap through both forms and sampling the canvas centre:

plain object equivalent ImageData
before [0, 0, 0, 255] — black [255, 255, 0, 255]
after [255, 255, 0, 255] [255, 255, 0, 255]

This matches the reporter's measurements. That probe was a throwaway and is not included — the committed test asserts the root cause (mipLevels) instead, since the repo keeps visual checks in test/render with golden images.

Suites run: core + layers headless — 84 files, 360 tests, all passing. biome check clean on all three files.

Not run locally: yarn test-render. The render project needs headed chromium, and the Playwright build this checkout pins (1217) currently 400s on every CDN mirror, so I could not install it. No existing golden image exercises the plain-object path — all bitmap-layer render cases load images by URL — so none should shift, but that is worth confirming in CI.

Also note the full headless suite is flaky on my machine independently of this change: repeated runs on unmodified master fail a varying 2–3 tests (LoadingWidget consistently, plus rotating WebGLAggregator/MapboxOverlay/react-mount failures). None are in core or layers.

`createTexture` reads `width`/`height` off `image.data`. Browser image
objects (ImageData, ImageBitmap, canvas, ...) are wrapped as
`{data: browserImage}` first, so their dimensions are found there. The
documented plain object form `{data: <Uint8Array>, width, height}` carries
them on the object itself, so both were `undefined` and
`device.getMipLevelCount()` returned `NaN`.

Combined with the default `mipmapFilter: 'linear'` sampler, the resulting
texture was incomplete and sampled as opaque black, painting black over
everything beneath the layer's bounds. Read the dimensions from whichever
level carries them.

Verified by pixel readback: a BitmapLayer using the plain object form
sampled `[0, 0, 0, 255]` before and `[255, 255, 0, 255]` after, matching the
equivalent `ImageData`.

Fixes visgl#10371
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.

BitmapLayer renders opaque black for the documented plain-object image form — createTexture reads width/height from image.data

1 participant