Skip to content

Fix text labels packed past TEXTURE_HEIGHT (clipped/stretched rendering) - #1314

Merged
devemux86 merged 4 commits into
mapsforge:masterfrom
jhotadhari:fix/TextBucket
Oct 24, 2026
Merged

devemux86 merged 4 commits into
mapsforge:masterfrom
jhotadhari:fix/TextBucket

Conversation

@jhotadhari

Copy link
Copy Markdown

Fixes #1313

Problem

Large captions occasionally rendered vertically clipped/stretched (flat cut at the bottom plus a halo smear), because TextBucket.prepare() only checked y + height > TEXTURE_HEIGHT inside the x + width > TEXTURE_WIDTH row-break branch. A label that fit horizontally into an existing bottom row was packed with its cell extending past the 256 px texture height: the glyphs were rasterized clipped at the bitmap's bottom edge, while the quad's texture coordinates still covered the full cell (out-of-bounds rows sample the last row via CLAMP_TO_EDGE).

Only the largest font styles were affected (smaller cells never exceed TEXTURE_HEIGHT), and the artifact came and went with panning/zooming because it depends on the atlas packing order for the visible label set — see the issue for screenshots and repro details.

Fix

Run the new-texture overflow check for every label instead of only on row breaks, so any cell that would exceed TEXTURE_HEIGHT starts a new texture:

if (x + width > TEXTURE_WIDTH) {
    x = 0;
    y += advanceY;
    advanceY = (int) (height + 0.5f);
}

if (y + height > TEXTURE_HEIGHT) {
    ... start new texture ...
}

One file changed (TextBucket.java, 16 insertions / 11 deletions). No API changes.

Verification

  • Instrumented build on device: atlas dumps + vertex/uv logs confirmed the clipped-cell mechanism (label cell 210–272.5 in a 256 px texture, glyphs cut at row 256).
  • With the fix, no reproduction across repeated pan/zoom sessions on the same device/map/theme.

Notes

  • The label pipeline derives the cell height from the cached TextStyle.fontHeight while the width is measured live via paint.measureText(). We considered (and tested) switching the height to live paint metrics as hardening — it's not required for this bug, but a tested reference implementation is available on my fork if maintainers are interested: jhotadhari/vtm branch keep/live-paint-text-metrics
  • This also fixes the same latent issue on any platform, not just Android.

Proposed CHANGELOG entry

- Fix text labels packed past TEXTURE_HEIGHT (clipped/stretched labels) [#XXXX](https://github.com/mapsforge/vtm/pull/XXXX)

(placed under ## Next version in docs/Changelog.md)

The new-texture overflow check (y + height > TEXTURE_HEIGHT) only ran
inside the row-break branch. A label that fit horizontally into an
existing row near the texture bottom was packed with its cell extending
past TEXTURE_HEIGHT, so drawText silently clipped its glyphs at the
bitmap's bottom edge while the quad still mapped the full cell. The
clipped glyphs plus the CLAMP_TO_EDGE smear of the last bitmap row
rendered the label vertically distorted on screen.

Run the overflow check for every label so oversized cells start a new
texture.
@devemux86

Copy link
Copy Markdown
Collaborator

Thank you, we need to check if this affects the performance on Android.

Regarding your first note:
It is best to make changes only when they are truly necessary and produce different results.
Given that performance is always a priority, it is best to avoid additional measurement calls.

@jhotadhari

Copy link
Copy Markdown
Author

Hi,

True, good point, we should ensure that it doesn't effect the performance.

I can't use my laptop the next days, maybe for a week, I'm sitting in mud. When I'm back in civilization, I can try to measure the performance.

Proposal: we can design it as opt-in. Maybe as a static property. Similar to org.oscim.backend.CanvasAdapter.textScale but boolean. If it affects performance, it has to be an opt-in feature, if implemented at all.

Regarding your first note:
It is best to make changes only when they are truly necessary and produce different results.

I agree.

@jhotadhari

Copy link
Copy Markdown
Author

Hi, I sent some AI agent friends to make the feature opt-in and implement and run tests. That's the result:

1. Opt-in flag

The fix is now opt-in via a new static flag CanvasAdapter.correctTextPacking (default false), modeled after CanvasAdapter.textScale. With the flag off (default), TextBucket.prepare() behaves exactly as before — the overflow check only runs when the flag is enabled. Existing users see zero change.

2. Tests

Added TextBucketPackingTest (JUnit 4, headless JVM, no GL) with no-op Canvas/Bitmap/Paint stubs. It builds synthetic label sets and verifies the packing invariant directly from the produced vertex UVs — no cell may exceed TEXTURE_HEIGHT × COORD_SCALE (2048) — plus texture-count deltas. Timing is printed, never asserted (CI-safe).

Note: vtm-tests is currently disabled in settings.gradle because its pre-existing suite fails on JDK 17 (MapEventLayerTest mockito reflection, OkHttpEngineTest okhttp version mismatch). The new test is included and can be run locally with:

# uncomment `include ':vtm-tests'` in settings.gradle
./gradlew :vtm-tests:test --tests org.oscim.renderer.bucket.TextBucketPackingTest
3. Results
JVM benchmark (300 labels per bucket, both flag states, deterministic seeds):
scenario	flag off	flag on
small labels — atlas overflow	none	none
small labels — textures	2	2
small labels — prepare()	~224 µs	~187 µs
tall captions (12/67 px) — atlas overflow	yes (2572 > 2048, bug)	none (2044 ≤ 2048)
tall captions — textures	32	33
tall captions — prepare()	~539 µs	~403 µs
prepare() runs only at bucket compile time (never per frame), so these sub-millisecond costs are negligible; the invariant is what matters.
On-device (Android, 120 Hz display, static buggy viewport with a visibly stretched label):
 	flag off	flag on
texture binds/frame (20 samples)	13	14
overflow packing	yes (label visibly stretched)	0 events
frame pacing	120 fps sustained	120 fps sustained
The only rendering cost is exactly one extra texture bind (+1 draw call) per frame, only when an overflow would have occurred — viewports without an overflowing cell are unchanged. Frame pacing unaffected. (Debug build with forced continuous rendering; the relative A/B is what counts.)

@devemux86 devemux86 added the bug label Oct 24, 2026
@devemux86 devemux86 added this to the 0.31.0 milestone Oct 24, 2026
@devemux86
devemux86 merged commit ac3e8f1 into mapsforge:master Oct 24, 2026
1 check passed
@devemux86

Copy link
Copy Markdown
Collaborator

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vertically clipped/stretched text labels when a label cell exceeds the texture atlas height

2 participants