Unrelated graphemes change Box clipping and framebuffer blending
Summary
Drawing a wide glyph in a disjoint part of the destination buffer changes two otherwise identical operations:
- A transparent-background Box stops writing outside its explicit scissor.
- A
respectAlpha: falseframebuffer copy changes from source replacement to alpha blending.
These are separate reproductions, not an interaction between the Box and framebuffer. Both use existing public APIs and the ordinary full-render implementation, without an incremental-render prototype.
Environment
- Commit:
202e1e6a0013252b6d0cd08c034e25f21d55f220. - Linux x86_64, Bun 1.3.14, Zig 0.16.0.
- Native library built from that commit with the root
bun run build(ReleaseFast). - Package version at that commit:
@opentui/core0.5.9.
The snippets below can run with Bun in a checkout's packages/core directory after building. They use the supported @opentui/core export. Shared setup for either snippet:
import { OptimizedBuffer, RGBA } from "@opentui/core"
const white = RGBA.fromInts(255, 255, 255)
const blue = RGBA.fromInts(0, 0, 255)
const red128 = RGBA.fromInts(255, 0, 0, 128)
const transparent = RGBA.fromInts(0, 0, 0, 0)Reproduction 1: Explicit Box Scissor
for (const wide of [false, true]) {
const dst = OptimizedBuffer.create(12, 4, "unicode")
try {
dst.clear(blue)
if (wide) dst.drawText("\u754c", 0, 3, white, blue)
dst.pushScissorRect(4, 1, 1, 1)
dst.drawBox({
x: 4, y: 1, width: 3, height: 1,
borderStyle: "single", border: true,
borderColor: white, backgroundColor: transparent,
})
dst.popScissorRect()
console.log({ wide, chars: [...dst.buffers.char.slice(16, 19)] })
} finally {
dst.destroy()
}
}Observed character codes at (4,1) through (6,1):
wide: false -> [9492, 9472, 9496]
wide: true -> [9492, 32, 32]Expected: [9492, 32, 32] in both cases. Only (4,1) is in the explicit scissor. This expectation does not add layout-bound clipping: arbitrary painting outside a renderable's layout should remain possible unless an explicit scissor excludes it.
The glyph at (0,3) is disjoint from both the Box and its scissor. The same discrepancy is also reproducible through createTestRenderer, using an overflow: "hidden" parent to establish the scissor and a preceding renderable to draw the disjoint glyph.
Source at the pinned commit:
canUseTransparentBorderFastPathchecks whole-buffer grapheme and link tracker presence.- Box rectangle/scissor intersection only establishes that some part intersects.
- Top/bottom direct border stores and vertical stores bypass per-cell scissor checks.
- The alternate setters check the scissor through
validateAndIndexor the alpha-blending setter.
Reproduction 2: Framebuffer Copy Mode
Run independently of the Box reproduction:
for (const wide of [false, true]) {
const dst = OptimizedBuffer.create(12, 4, "unicode")
const src = OptimizedBuffer.create(1, 1, "unicode", { respectAlpha: false })
try {
dst.clear(blue)
if (wide) dst.drawText("\u754c", 0, 3, white, blue)
src.setCell(0, 0, "X", white, red128)
dst.drawFrameBuffer(4, 1, src)
console.log({ wide, bg: [...dst.buffers.bg.slice(64, 68)] })
} finally {
src.destroy()
dst.destroy()
}
}Observed RGBA bytes at (4,1):
wide: false -> [255, 0, 0, 128]
wide: true -> [128, 0, 127, 255]The character remains X in both cases. The first background is the source value copied verbatim; the second is the source background blended over blue.
Expected at the default global opacity of 1: with source respectAlpha: false, copy the source cell, including its alpha and color metadata, regardless of disjoint tracker contents. With source respectAlpha: true, use alpha composition consistently. The flag should select semantics; resource tracking should only ensure safe ownership and wide-cell handling. This is not a claim that blending is always wrong.
Source at the pinned commit:
- Source flag and whole-buffer grapheme/link/image state select memcpy.
- Fallback skips fully transparent ordinary cells and unconditionally calls blending setters, without consulting source
respectAlpha. - Existing tests distinguish alpha-aware framebuffer composition and metadata-preserving copy.
Related source-level detail: drawFrameBuffer returns at zero global opacity, but the memcpy condition does not exclude partial global opacity, whereas fallback blending applies the effective opacity. A correction should explicitly cover that boundary rather than replace all copies with blending or ignore opacity globally.
Scope
The measured trigger here is a disjoint wide grapheme. Link tracking is also present in the fast-path predicates, but the snippets above do not measure a link-triggered reproduction. Image handling has additional semantics and is not established by these probes.
This came up while establishing full-render reference behavior for an isolated rendering experiment: identical local inputs, scissor, and contributions can change output because an unrelated earlier draw changes whole-buffer tracker state. An experimental-branch-only correction is planned, with regressions for both scenes and copy/alpha/opacity boundaries. No correction has been applied to main, and this report does not claim the fix is already complete.
Source: anomalyco/opentui