Skip to content

feat: 2d UI revamp#223

Open
schell wants to merge 12 commits intomainfrom
feat/2d-ui-revamp
Open

feat: 2d UI revamp#223
schell wants to merge 12 commits intomainfrom
feat/2d-ui-revamp

Conversation

@schell
Copy link
Owner

@schell schell commented Mar 18, 2026

Summary

Replace the old renderling::ui module (a thin wrapper around the full 3D Stage that ran every UI pixel through the PBR fragment shader) with a new (really an old crate but everything old is new again) standalone renderling-ui crate built on a lightweight, purpose-built GPU pipeline.

Motivation

The old UI system had significant overhead: 13 GPU bindings, ~160-byte vertices, and the full PBR fragment shader for flat 2D elements. The new system uses 3 GPU bindings, 32-byte vertices, SDF-based shape rendering, and a dedicated vertex/fragment shader pair — making it dramatically simpler and more appropriate for 2D/UI workloads.

What's New

renderling-ui crate

  • SDF shapes: UiRect (with rounded corners), UiCircle, UiEllipse — resolution-independent, anti-aliased via SDF evaluation per pixel
  • Gradients & borders: Linear/radial gradients and configurable borders on all SDF shapes
  • Image rendering: Atlas-based image display with tint and opacity via UiImage
  • Text rendering: glyph_brush integration with per-glyph atlas texture descriptors via UiText
  • Path rendering: Lyon-based vector paths with fill, stroke, and image-fill support via UiPath
  • 4x MSAA enabled by default (configurable via with_msaa_sample_count)
  • Z-sorted draw calls (painter's algorithm, sorted every frame)
  • Property getters on all element types for reading current values back
  • 14 visual regression tests with baseline images

renderling crate changes

  • New compositor module: Fullscreen-quad alpha-blending compositor (rust-gpu shaders) for overlaying MSAA-resolved UI content on top of 3D scenes without overwriting existing framebuffer content
  • New ui_slab module: Shared GPU/CPU types (UiVertex, UiDrawCallDescriptor, UiViewport, UiElementType, GradientDescriptor) used by both the shader and CPU code
  • Removed old ui module: Deleted ui.rs, ui/cpu.rs, ui/sdf.rs, ui/cpu/text.rs, ui/cpu/path.rs and the ui feature flag
  • Typed slab IDs: atlas_texture_id is now Id, atlas_descriptor_id is now Id

Example crate

  • Migrated from renderling::ui::Ui to renderling_ui::UiRenderer
  • FPS overlay now composites correctly on top of the 3D stage

schell added 7 commits March 9, 2026 12:13
Introduces a dedicated lightweight 2D rendering pipeline separate from
the 3D PBR pipeline. Uses SlabAllocator for GPU memory management with
Hybrid<UiDrawCallDescriptor> wrapper types (UiRect, UiCircle, UiEllipse)
following the Camera pattern for live GPU updates via set_*/with_* methods.

- SDF-based shape rendering (rect, rounded rect, circle, ellipse)
- Gradient fills (linear, radial), borders, anti-aliased edges
- 3 GPU bindings and 32-byte vertices (vs 13 bindings / ~160 bytes for 3D)
- Instanced quads with per-pixel SDF evaluation in fragment shader
- 6 visual regression tests with baseline images
Add UiImage element type with atlas texture support, enabling
texture/image rendering in the 2D UI pipeline. Fix critical bug
where Atlas::new() was allocated before UiViewport on the slab,
causing the shader (which reads viewport at offset 0) to read
garbage data and render blank white output.

- Add UiImage wrapper with set/with builders for position, size,
  tint, opacity, z-depth
- Replace dummy atlas with real Atlas integration (512x512x2)
- Add atlas_size field to UiViewport for correct UV mapping
- Fix shader to use viewport.atlas_size instead of viewport.size
  for AtlasTextureDescriptor::uv() calls
- Add AtlasImage, AtlasTexture, UiImage to public re-exports
- Recompile shaders with updated UiViewport layout
- Add 2 image rendering tests (checkerboard + tint)
- Regenerate baseline images with correct rendering
- 8 renderling-ui tests + 101 renderling tests pass
Port text rendering from the old UI module to the new lightweight
2D pipeline. Glyph rasterization uses glyph_brush to produce a
Luma8 cache image, which is converted to RGBA (white + alpha) and
uploaded to the atlas. Each visible glyph gets its own
AtlasTextureDescriptor pointing to its sub-region in the cache,
and a UiDrawCallDescriptor with element_type TextGlyph. The
existing fragment shader samples glyph coverage from the atlas
alpha channel and multiplies by fill_color.

- Add GlyphCache wrapping GlyphBrush with Luma8 cache image
- Add UiText handle type with set_z/set_opacity methods
- Add add_font(), add_text(), remove_text() to UiRenderer
- Add image as optional dep behind 'text' feature
- Re-export FontArc, FontId, Section, Text, UiText
- Add 2 text tests (plain text + text overlaid on a rounded rect)
- 10 renderling-ui tests + 101 renderling tests pass, 0 clippy warnings
Port path/vector rendering from the old UI module to the new
lightweight 2D pipeline. Uses lyon for tessellation (fill and
stroke), de-indexes the triangle mesh, and writes UiVertex arrays
to the slab. The existing vertex shader's Path branch reads
pre-tessellated vertices from the slab, and the fragment shader
passes through the interpolated vertex color.

- Add UiPathBuilder with lyon path commands (begin, line_to,
  quadratic/cubic bezier, add_rectangle, add_circle, etc.)
- Add fill() and stroke() methods for tessellation
- Add StrokeConfig (line width, cap, join) configuration
- Add UiPath handle type with z-depth and opacity setters
- Add path_builder() and remove_path() to UiRenderer
- Re-export StrokeConfig, UiPath, UiPathBuilder behind 'path' feature
- Add 3 path tests (filled triangle, stroked circle, mixed shapes)
- 13 renderling-ui tests + 101 renderling tests pass, 0 clippy warnings
…ling-ui

Delete the old renderling::ui module (cpu.rs, sdf.rs, text.rs, path.rs) and
its test baselines, remove the 'ui' feature flag and its optional deps
(glyph_brush, lyon, loading-bytes). Move loading-bytes to dev-dependencies
since it's still needed by wasm tests. Update the example crate to use
renderling-ui's UiRenderer, UiRect, UiText, and Section APIs instead of
the removed Ui type.
Add UiPathBuilder::with_fill_image() for filling tessellated paths with
atlas textures. The shader samples the atlas when atlas_texture_id is set
on Path elements, using bounding-box-derived UVs. Add UiRenderer::upload_image()
to load images without creating a draw call.

Enable 4x MSAA by default (matching the old Ui behavior) and expose
with_msaa_sample_count() for configuration.

Replace raw u32 fields in UiDrawCallDescriptor with typed Ids:
atlas_texture_id becomes Id<AtlasTextureDescriptor> and
atlas_descriptor_id becomes Id<AtlasDescriptor>, using Id::NONE
as the unset sentinel. Update shader and CPU code accordingly.
Add a Compositor to the renderling crate (compositor_vertex +
compositor_fragment shaders) that alpha-blends a source texture onto a
target framebuffer via a fullscreen quad. This fixes the black screen
when overlaying the UI renderer on a 3D stage with MSAA enabled: the UI
now renders to an intermediate texture, resolves MSAA there, and the
compositor blends it onto the final view preserving the scene beneath.

Add property getters to all UI element types (UiRect, UiCircle,
UiEllipse, UiImage, UiPath, UiText) so current values can be read back
— a prerequisite for the upcoming tweening/animation system.

Fix the example app's UI renderer clearing the 3D scene by removing the
erroneous .with_background_color(Vec4::ZERO) call, which was causing a
LoadOp::Clear that wiped the stage output.
@schell schell marked this pull request as ready for review March 18, 2026 04:04
@schell schell requested a review from Copilot March 18, 2026 04:05
Copy link

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

This PR replaces the legacy renderling::ui (3D-stage-based) UI path with a dedicated 2D UI pipeline via a new renderling-ui crate, backed by shared CPU/GPU slab types and new rust-gpu shaders, plus a compositor path for overlaying UI over 3D.

Changes:

  • Added renderling-ui crate (SDF shapes, gradients, images, optional text/path) and visual regression tests/baselines.
  • Added renderling::ui_slab shared slab types + UI vertex/fragment shaders and generated linkage artifacts.
  • Added renderling::compositor (fullscreen-quad alpha blend) and migrated the example to UiRenderer; removed the old renderling::ui module/feature.

Reviewed changes

Copilot reviewed 26 out of 53 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Cargo.toml Adds crates/renderling-ui workspace member.
Cargo.lock Adds renderling-ui package to lockfile graph.
crates/renderling/Cargo.toml Removes legacy ui feature/deps; adjusts dev-deps.
crates/renderling/src/lib.rs Exports new compositor + ui_slab; removes legacy ui module export.
crates/renderling/src/context.rs Removes Context::new_ui() legacy constructor.
crates/renderling/src/ui_slab/mod.rs Introduces shared UI slab types (CPU/GPU).
crates/renderling/src/ui_slab/shader.rs Adds rust-gpu UI vertex/fragment shaders.
crates/renderling/src/compositor.rs Adds compositor rust-gpu shaders + CPU re-export.
crates/renderling/src/compositor/cpu.rs Adds CPU-side compositor pipeline for overlay blending.
crates/renderling/src/linkage.rs Registers new compositor + UI shader linkage modules.
crates/renderling/src/linkage/ui_vertex.rs Adds generated linkage for UI vertex shader.
crates/renderling/src/linkage/ui_fragment.rs Adds generated linkage for UI fragment shader.
crates/renderling/src/linkage/compositor_vertex.rs Adds generated linkage for compositor vertex shader.
crates/renderling/src/linkage/compositor_fragment.rs Adds generated linkage for compositor fragment shader.
crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth_multisampled.rs Formatting-only update to generated include paths.
crates/renderling/shaders/manifest.json Registers new compositor + UI SPIR-V artifacts.
crates/renderling/shaders/compositor-compositor_vertex.spv Adds compiled SPIR-V for compositor vertex shader.
crates/renderling/shaders/compositor-compositor_fragment.spv Adds compiled SPIR-V for compositor fragment shader.
crates/renderling/shaders/ui_slab-shader-ui_vertex.spv Adds compiled SPIR-V for UI vertex shader.
crates/renderling/shaders/ui_slab-shader-ui_fragment.spv Adds compiled SPIR-V for UI fragment shader.
crates/renderling/src/ui.rs Removes legacy UI module root.
crates/renderling/src/ui/cpu.rs Removes legacy CPU UI implementation.
crates/renderling/src/ui/cpu/path.rs Removes legacy path UI implementation.
crates/renderling/src/ui/cpu/text.rs Removes legacy text UI implementation.
crates/renderling/src/ui/sdf.rs Removes legacy UI SDF stub.
crates/renderling-ui/Cargo.toml Adds new renderling-ui crate configuration/features.
crates/renderling-ui/src/lib.rs Adds renderling-ui public API + re-exports.
crates/renderling-ui/src/renderer.rs Implements UiRenderer and element builders.
crates/renderling-ui/src/test.rs Adds visual regression tests for the new UI renderer.
crates/example/Cargo.toml Adds dependency on renderling-ui.
crates/example/src/lib.rs Migrates example UI usage to renderling_ui::UiRenderer.
test_img/ui2d/rect.png New baseline image for 2D UI rect test.
test_img/ui2d/rounded_rect.png New baseline image for rounded rect test.
test_img/ui2d/circle.png New baseline image for circle test.
test_img/ui2d/bordered_rect.png New baseline image for bordered rect test.
test_img/ui2d/multiple_shapes.png New baseline image for multiple shapes test.
test_img/ui2d/image.png New baseline image for image test.
test_img/ui2d/image_tint.png New baseline image for tinted image test.
test_img/ui2d/gradient_rect.png New baseline image for gradient rect test.
test_img/ui2d/text.png New baseline image for text test.
test_img/ui2d/text_with_shapes.png New baseline image for text-over-shape test.
test_img/ui2d/filled_path.png New baseline image for filled path test.
test_img/ui2d/stroked_path.png New baseline image for stroked path test.
test_img/ui2d/path_shapes.png New baseline image for path shapes test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +251 to +265
if draw_call.border_width > 0.0 {
// Border: the border region is between the outer edge and
// (outer edge - border_width).
let inner_distance = distance + draw_call.border_width;
let border_alpha =
1.0 - crate::math::smoothstep(-aa_width, aa_width, inner_distance);
// Inside the border but outside the fill = border color.
let in_border = fill_alpha;
let in_fill = border_alpha;
color = draw_call.border_color * (in_border - in_fill) + fill * in_fill;
color.w = draw_call.border_color.w * (in_border - in_fill) + fill.w * in_fill;
} else {
color = fill;
color.w *= fill_alpha;
}
Comment on lines +166 to +175
/// Scissor/clip rectangle (x, y, width, height).
/// Elements outside this rect are clipped. Set to (0,0, viewport_w,
/// viewport_h) for no clipping.
pub clip_rect: Vec4,
/// Element opacity (0.0 = fully transparent, 1.0 = fully opaque).
/// Multiplied with the final alpha.
pub opacity: f32,
/// Z-depth for sorting (painter's algorithm). Lower values are drawn
/// first (further back).
pub z: f32,
schell and others added 5 commits March 19, 2026 07:45
Address all 5 Copilot review comments from PR #223:

- Switch entire UI pipeline and compositor to premultiplied-alpha
  blending. The fragment shader now premultiplies RGB by final alpha
  before output, and both the UI pipeline and compositor use
  PREMULTIPLIED_ALPHA_BLENDING. This fixes edge darkening on borders
  and correct compositing of MSAA-resolved overlay textures.

- Fix border coverage in the fragment shader: compute straight-alpha
  weighted blend of border and fill colors, then premultiply at the
  end, avoiding the previous double-application of alpha at AA edges.

- Use explicit entry points (Some(linkage.entry_point)) in the UI
  render pipeline for consistency with the rest of the codebase.

- Update clip_rect doc to note it is reserved for future use (not
  currently enforced by the shader or renderer).

- Fix ui_vertex doc comment to only mention Path elements reading
  from the slab (TextGlyph uses the standard quad generation path).
…scene

Split the scene node query API into two explicit methods:

- root_nodes_in_scene: returns only the top-level nodes directly
  referenced by the scene (the original behavior)
- recursive_nodes_in_scene: returns all nodes including descendants
  in depth-first order

This fixes a bug where animations targeting child (parented) nodes
were silently skipped because only root nodes were collected.

Update call sites:
- Animator test now uses recursive_nodes_in_scene for correctness
- Example crate simplified by removing manual get_children helper

Co-authored-by: Andreas Streichardt <andreas@mop.koeln>
@schell schell force-pushed the feat/2d-ui-revamp branch from 9c4baa9 to c0e1ded Compare March 18, 2026 22:31
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.

3 participants