Skip to content

feat: 2d UI revamp - #223

Merged
schell merged 14 commits into
mainfrom
feat/2d-ui-revamp
Mar 21, 2026
Merged

feat: 2d UI revamp#223
schell merged 14 commits into
mainfrom
feat/2d-ui-revamp

Conversation

@schell

Copy link
Copy Markdown
Owner

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
schell marked this pull request as ready for review March 18, 2026 04:04
@schell
schell requested a review from CopilotMarch 18, 2026 04:05

CopilotAI left a comment

Copy link
Copy Markdown

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
FileDescription
Cargo.tomlAdds crates/renderling-ui workspace member.
Cargo.lockAdds renderling-ui package to lockfile graph.
crates/renderling/Cargo.tomlRemoves legacy ui feature/deps; adjusts dev-deps.
crates/renderling/src/lib.rsExports new compositor + ui_slab; removes legacy ui module export.
crates/renderling/src/context.rsRemoves Context::new_ui() legacy constructor.
crates/renderling/src/ui_slab/mod.rsIntroduces shared UI slab types (CPU/GPU).
crates/renderling/src/ui_slab/shader.rsAdds rust-gpu UI vertex/fragment shaders.
crates/renderling/src/compositor.rsAdds compositor rust-gpu shaders + CPU re-export.
crates/renderling/src/compositor/cpu.rsAdds CPU-side compositor pipeline for overlay blending.
crates/renderling/src/linkage.rsRegisters new compositor + UI shader linkage modules.
crates/renderling/src/linkage/ui_vertex.rsAdds generated linkage for UI vertex shader.
crates/renderling/src/linkage/ui_fragment.rsAdds generated linkage for UI fragment shader.
crates/renderling/src/linkage/compositor_vertex.rsAdds generated linkage for compositor vertex shader.
crates/renderling/src/linkage/compositor_fragment.rsAdds generated linkage for compositor fragment shader.
crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth_multisampled.rsFormatting-only update to generated include paths.
crates/renderling/shaders/manifest.jsonRegisters new compositor + UI SPIR-V artifacts.
crates/renderling/shaders/compositor-compositor_vertex.spvAdds compiled SPIR-V for compositor vertex shader.
crates/renderling/shaders/compositor-compositor_fragment.spvAdds compiled SPIR-V for compositor fragment shader.
crates/renderling/shaders/ui_slab-shader-ui_vertex.spvAdds compiled SPIR-V for UI vertex shader.
crates/renderling/shaders/ui_slab-shader-ui_fragment.spvAdds compiled SPIR-V for UI fragment shader.
crates/renderling/src/ui.rsRemoves legacy UI module root.
crates/renderling/src/ui/cpu.rsRemoves legacy CPU UI implementation.
crates/renderling/src/ui/cpu/path.rsRemoves legacy path UI implementation.
crates/renderling/src/ui/cpu/text.rsRemoves legacy text UI implementation.
crates/renderling/src/ui/sdf.rsRemoves legacy UI SDF stub.
crates/renderling-ui/Cargo.tomlAdds new renderling-ui crate configuration/features.
crates/renderling-ui/src/lib.rsAdds renderling-ui public API + re-exports.
crates/renderling-ui/src/renderer.rsImplements UiRenderer and element builders.
crates/renderling-ui/src/test.rsAdds visual regression tests for the new UI renderer.
crates/example/Cargo.tomlAdds dependency on renderling-ui.
crates/example/src/lib.rsMigrates example UI usage to renderling_ui::UiRenderer.
test_img/ui2d/rect.pngNew baseline image for 2D UI rect test.
test_img/ui2d/rounded_rect.pngNew baseline image for rounded rect test.
test_img/ui2d/circle.pngNew baseline image for circle test.
test_img/ui2d/bordered_rect.pngNew baseline image for bordered rect test.
test_img/ui2d/multiple_shapes.pngNew baseline image for multiple shapes test.
test_img/ui2d/image.pngNew baseline image for image test.
test_img/ui2d/image_tint.pngNew baseline image for tinted image test.
test_img/ui2d/gradient_rect.pngNew baseline image for gradient rect test.
test_img/ui2d/text.pngNew baseline image for text test.
test_img/ui2d/text_with_shapes.pngNew baseline image for text-over-shape test.
test_img/ui2d/filled_path.pngNew baseline image for filled path test.
test_img/ui2d/stroked_path.pngNew baseline image for stroked path test.
test_img/ui2d/path_shapes.pngNew baseline image for path shapes test.

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

Comment threadcrates/renderling/src/ui_slab/shader.rs Outdated
Comment threadcrates/renderling-ui/src/renderer.rs
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 threadcrates/renderling/src/compositor/cpu.rs Outdated
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 added a commit that referenced this pull request Mar 18, 2026
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).
schell added a commit that referenced this pull request Mar 20, 2026
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).
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.
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).
@schell
schell merged commit a7b44f7 into mainMar 21, 2026
9 checks passed
@schell
schell deleted the feat/2d-ui-revamp branch March 21, 2026 20:14
Sign up for freeto 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.

2 participants

@schell