Skip to content

Polyfills/Canvas: fix invisible text, font-load races, and blit ordering#1683

Merged
bkaradzic-microsoft merged 1 commit intoBabylonJS:masterfrom
bkaradzic-microsoft:master
Apr 29, 2026
Merged

Polyfills/Canvas: fix invisible text, font-load races, and blit ordering#1683
bkaradzic-microsoft merged 1 commit intoBabylonJS:masterfrom
bkaradzic-microsoft:master

Conversation

@bkaradzic-microsoft
Copy link
Copy Markdown
Contributor

@bkaradzic-microsoft bkaradzic-microsoft commented Apr 28, 2026

Several long-standing NativeCanvas bugs caused text to be invisible or mis-rendered, fonts loaded after Context creation to be ignored, and canvas-to-texture copies to capture stale/empty content when more than one canvas was flushed per frame.

Context (Polyfills/Canvas/Source/Context.{h,cpp}):

  • Add EnsureFontsLoaded() and call it from Flush, SetFont, SetFontFaceId, and MeasureText. Previously fonts were registered with nanovg only in the Context constructor, so any font loaded after a Context was created (the common case for Babylon GUI) was silently ignored and text rendered with the default font id of -1.
  • Save()/Restore() now stack m_fillStyle and m_strokeStyle on a new m_savedStyles vector alongside nvg's own state stack. Babylon GUI brackets fillStyle changes with save/restore; without wrapper-side stacking the previous draw's fill colour leaked across the restore and u_innerCol was carried over from the prior draw call.
  • BindFillStyle: treat an empty fillStyle string as opaque white instead of transparent black, matching browser behaviour for an un-set fillStyle.
  • MeasureText: when the requested font family is not loaded, return Arial-equivalent metrics (~0.55 * em * length) instead of zero. Babylon GUI uses MeasureText results to centre text; a zero width combined with the bug below pushed glyphs entirely off-canvas.

MeasureText (Polyfills/Canvas/Source/MeasureText.cpp):

  • Zero-initialise bounds[4] and textMetrics[3] before calling nvgTextBounds / nvgTextMetrics. nanovg does not always populate every slot (in particular when no font face is bound), and the uninitialised stack values were being returned to JS as widths in the hundreds of millions, producing fillText x ≈ -1e8 and clipping text such as the GUI "Click Me" button completely off-screen. This single fix is the root cause of the most visible regression.

Shaders (Polyfills/Canvas/Source/Shaders/*.sc and regenerated headers for dxbc/dxil/essl/glsl/metal/spirv):

  • Remove the redundant u_viewSize uniform from vs_nanovg_fill, fs_nanovg_fill, vs_fspass, fs_boxblur and fs_gaussblur. bgfx already exposes viewport dimensions via the predefined u_viewRect uniform whose .zw components are width/height, so u_viewSize was a duplicate that had to be set every frame from C++ and was easy to forget.
  • nanovg/nanovg_babylon.cpp: drop the matching UniformHandle field, createUniform, setUniform and destroy calls.

NativeEngine + DeviceContext (Plugins/NativeEngine/Source/NativeEngine.cpp, Core/Graphics/...):

  • Add DeviceContext::PeekNextViewId() that returns the next view id without consuming it, and use it in CopyTexture to schedule the bgfx blit on a view id that is greater than every view used so far. bgfx processes blits in numeric view-id order, so the previous code (which queued blits on view 0) ran the copy before the canvas Flushes that produced the source framebuffers had been submitted, and the destination texture received empty content. This manifested in tests like gltfAnimationSkinType where only the first dynamic label ("00") was visible and the rest were blank, even though four distinct Context/canvas/framebuffer instances were correctly created and flushed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes several NativeCanvas rendering issues around text visibility/metrics, font-load timing, style save/restore correctness, and canvas-to-texture copy ordering; also removes a redundant shader uniform by switching to bgfx’s predefined u_viewRect.

Changes:

  • Canvas Context: add EnsureFontsLoaded() and style save/restore stacking; adjust empty fillStyle default and improve measureText behavior.
  • MeasureText: zero-initialize NanoVG metric buffers to prevent garbage widths.
  • Rendering pipeline: remove u_viewSize uniform usage across shaders/nanovg glue; adjust CopyTexture blit view-id scheduling via DeviceContext::PeekNextViewId().

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Polyfills/Canvas/Source/nanovg/nanovg_babylon.cpp Removes u_viewSize uniform plumbing and per-pass uniform sets.
Polyfills/Canvas/Source/Shaders/vs_nanovg_fill.sc Switches math from u_viewSize to predefined u_viewRect.
Polyfills/Canvas/Source/Shaders/vs_fspass.sc Removes unused u_viewSize uniform.
Polyfills/Canvas/Source/Shaders/fs_nanovg_fill.sc Removes unused u_viewSize uniform.
Polyfills/Canvas/Source/Shaders/fs_gaussblur.sc Uses u_viewRect.zw for texel size (drops u_viewSize).
Polyfills/Canvas/Source/Shaders/fs_boxblur.sc Uses u_viewRect.zw for texel size (drops u_viewSize).
Polyfills/Canvas/Source/Shaders/spirv/vs_fspass.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/spirv/fs_boxblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/metal/vs_nanovg_fill.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/metal/vs_fspass.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/metal/fs_gaussblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/metal/fs_boxblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/glsl/vs_nanovg_fill.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/glsl/fs_gaussblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/glsl/fs_boxblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/essl/vs_nanovg_fill.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/essl/fs_gaussblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/essl/fs_boxblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/dxil/vs_nanovg_fill.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/dxil/fs_gaussblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/dxil/fs_boxblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/dxbc/vs_nanovg_fill.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/dxbc/fs_gaussblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/Shaders/dxbc/fs_boxblur.h Regenerated shader binary/header reflecting uniform changes.
Polyfills/Canvas/Source/MeasureText.cpp Zero-initializes NanoVG bounds/metrics buffers before calls.
Polyfills/Canvas/Source/Context.h Adds EnsureFontsLoaded() declaration and style stack storage.
Polyfills/Canvas/Source/Context.cpp Implements font refresh, style stacking, fillStyle defaulting, and measureText fallback behavior.
Plugins/NativeEngine/Source/NativeEngine.cpp Changes CopyTexture to use encoder->blit with a later view id.
Core/Graphics/Source/DeviceImpl.h Exposes PeekNextViewId() on graphics implementation.
Core/Graphics/Source/DeviceImpl.cpp Implements PeekNextViewId() via atomic load.
Core/Graphics/Source/DeviceContext.cpp Adds DeviceContext::PeekNextViewId() forwarding call.
Core/Graphics/InternalInclude/Babylon/Graphics/DeviceContext.h Adds PeekNextViewId() to the public DeviceContext API.
Comments suppressed due to low confidence (1)

Polyfills/Canvas/Source/Context.cpp:621

  • SetFontFaceId() calls EnsureFontsLoaded(), but if m_currentFontId is -1 because the requested family wasn't available when ctx.font was set, this function will still fall back to m_fonts.begin() even if the requested family has since become available. To actually resolve the font-load race, consider: when m_currentFontId < 0, attempt to look up m_font.Familiy() in m_fonts after EnsureFontsLoaded() and bind that id when present (otherwise fall back).
    bool Context::SetFontFaceId()
    {
        EnsureFontsLoaded();
        if (m_fonts.empty())
        {
            return false;
        }
        else if (m_currentFontId >= 0)
        {
            nvgFontFaceId(*m_nvg, m_currentFontId);
        }
        else
        {
            nvgFontFaceId(*m_nvg, m_fonts.begin()->second);
        }

Comment thread Polyfills/Canvas/Source/Context.cpp
Comment thread Polyfills/Canvas/Source/Context.cpp
Comment thread Plugins/NativeEngine/Source/NativeEngine.cpp
@bkaradzic-microsoft bkaradzic-microsoft enabled auto-merge (squash) April 28, 2026 17:56
Copy link
Copy Markdown
Collaborator

@CedricGuillemet CedricGuillemet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@bkaradzic-microsoft bkaradzic-microsoft merged commit 3746203 into BabylonJS:master Apr 29, 2026
57 of 58 checks passed
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.

4 participants