oxiui-render-wgpu 0.1.2

wgpu GPU render surface for OxiUI
Documentation
# oxiui-render-wgpu TODO

## Status
Real headless GPU foundation landed (2026-05-30). On top of the CPU preparation engine (atlas/batch/clip/quality/resource/error), the crate now has a working `WgpuBackend` (`src/gpu/`) that implements `oxiui-core`'s `RenderBackend` on an offscreen wgpu target: solid `FillRect`, SDF `FillCircle`, and `PushClip`/`PopClip` scissor clipping, with byte-exact CPU pixel readback. Initialised headlessly (no window/surface); gracefully reports `UiError::Unsupported` when no GPU adapter is present. wgpu is pinned to 29.0.3 to unify with eframe's existing wgpu (no duplicate build). The legacy `WgpuRenderer` PhantomData stub is retained for compatibility. Still to come for full framework independence: textured/gradient/shadow/text fragment variants, MSAA, instancing, and a window-owning swap-chain surface + present-mode selection.

## Core Implementation
- [x] wgpu device/queue initialization (done 2026-05-30): `WgpuRenderer::init(window_handle)` acquiring adapter, device, queue, surface configuration, present mode selection (Fifo/Mailbox/Immediate) (~200 SLOC) (planned 2026-05-30)
  - **Landed:** `WgpuBackend::headless(w,h)` in `src/gpu/device.rs` — `Instance::default()` → `request_adapter` (via `pollster::block_on`; `Err`→`UiError::Unsupported` so callers skip) → `request_device`/`queue` → offscreen `Rgba8Unorm` (linear, byte-exact readback) texture with `RENDER_ATTACHMENT | COPY_SRC`. No window/surface (present-mode selection deferred to the window-owning milestone). wgpu pinned to 29.0.3 (unifies with eframe's wgpu, no duplicate build).
  - **Goal:** real (non-stub) GPU backend implementing oxiui-core RenderBackend for FillRect/FillCircle/clip; headless offscreen texture (no window); pixel-readback tests that actually run on this Mac's Metal backend (graceful skip if no adapter).
  - **Design:** add workspace deps wgpu/pollster/bytemuck (latest, pure Rust). WgpuBackend::headless(w,h): Instance→request_adapter (pollster::block_on)→device/queue→offscreen Rgba8UnormSrgb target. execute(&DrawList) replays FillRect, FillCircle (SDF in fragment), PushClip/PopClip→set_scissor_rect via the existing tested ClipStack. solid.wgsl: vertex [pos vec2, color vec4] + 2D ortho, fragment solid fill + circle SDF. readback_rgba() (texture→buffer→map) for tests. supports_text/images/blur/gradients=false this slice.
  - **Files:** crates/oxiui-render-wgpu/src/gpu/{device,renderer,pipeline,buffer}.rs, src/shaders/solid.wgsl, tests/headless_render_tests.rs; src/lib.rs exports the gpu module; Cargo.toml + root [workspace.dependencies] gain wgpu/pollster/bytemuck. Each file <2000 lines.
  - **Tests:** offscreen render → readback → assert RGBA at known coords (red rect @(10,10); circle center; clipped region unaffected). Adapter guard: `if adapter.is_none() { return }` so no-GPU CI passes cleanly; on this darwin/Metal host the tests must ACTUALLY execute — report ran-vs-skipped.
  - **Risk:** GPU availability (mitigated by skip + Metal here), async device init (pollster::block_on), vertex alignment (bytemuck::Pod + a size assertion), wgpu version drift (reconcile to one version if eframe/iced in the workspace pull a different wgpu). std-only. No unwrap in production paths.
- [x] Render pipeline setup (done 2026-05-30): vertex/fragment shader module creation, pipeline layout with bind groups, vertex buffer layout (position, UV, color), render pass configuration (~250 SLOC) — (see wgpu foundation plan above)
  - **Landed:** `src/gpu/pipeline.rs` (`SolidPipeline`) — `solid.wgsl` module, uniform bind-group layout (viewport `Globals`), pipeline layout, alpha-blended `RenderPipeline`, hand-derived vertex attribute layout matching the `Vertex` offsets; `src/gpu/buffer.rs` defines the `#[repr(C)]` `Pod`/`Zeroable` `Vertex`/`Globals` with compile-time size asserts. Textured/UV variants deferred (this slice is solid + circle SDF only).
- [x] WGSL shader for UI primitives (done 2026-05-30): vertex shader (2D transform + projection), fragment shader (solid color, textured, rounded-rect SDF, gradient), preprocessor-like variant selection (~300 SLOC) — (see wgpu foundation plan above)
  - **Landed:** `src/shaders/solid.wgsl` — vertex stage applies a 2-D orthographic projection (pixel coords → NDC); fragment emits solid colour for rects and a `smoothstep` circle-SDF coverage for circles, switched on a per-vertex `kind` discriminator. Textured/rounded-rect/gradient fragment variants remain deferred to later slices.
- [x] Rounded rectangle SDF rendering (done prior to 2026-06-02): SDF kind=2 (uniform radius) and kind=3 (per-corner packed radii) in solid.wgsl; border-colour/border-width variant deferred (would require a new DrawCommand field).
- [x] **wgpu CPU foundations: texture atlas, draw-call batcher, clip stack, quality, resource handles, error mapping** (completed 2026-05-29)
  - **Goal:** turn the 32-line PhantomData stub into the GPU backend's full CPU-side preparation engine — everything a future GPU `RenderBackend` impl will compose, built and unit-tested with no GPU and no `wgpu` dependency.
  - **Design:** new modules in `crates/oxiui-render-wgpu/src/`: `atlas.rs` (shelf+skyline bin-packer, LRU eviction, utilization metric), `batch.rs` (DrawList batcher: BatchKey sort + merge into DrawBatch + visibility culling), `clip.rs` (nested clip-rect stack with intersection, integer-rect scissor), `quality.rs` (RenderQuality{msaa,shadow,text} enums + Low/Balanced/High presets), `resource.rs` (TextureHandle/ShaderHandle newtypes + Rc refcount ResourceRegistry + RAII Drop), `error.rs` (GpuErrorKind{DeviceLost,OOM,ShaderCompile,SurfaceLost} + map_gpu_error→UiError). `lib.rs` ties all together with WgpuPrep::prepare(&DrawList)->PreparedFrame. No RenderBackend impl yet (GPU tail). No new deps.
  - **Files:** new `src/{atlas.rs,batch.rs,clip.rs,quality.rs,resource.rs,error.rs}`; rewrite stub `src/lib.rs`. No Cargo.toml changes.
  - **Prerequisites:** none (oxiui_core::paint::{DrawList,DrawCommand,Rect} exist).
  - **Tests (~16):** atlas packs 100 random rects no-overlap + util>70%; LRU eviction invariants; batcher 1000 rects/5 textures→≤5 batches; stable order; clip push/pop/intersection; underflow saturates; culling drops off-screen; quality presets; resource refcount+RAII; map_gpu_error covers all; prepare(empty) is noop.
  - **Risk:** self-contained (no GPU, no external API). Batcher sort/merge is the subtle part — test order-stability. Defer all GPU-runtime items.
- [x] Draw call batching: sort draw commands by texture/shader/blend-state, merge adjacent draws with the same state into single `draw_indexed` calls, reduce GPU state changes (~200 SLOC)
  - **Goal:** N consecutive gradients collapse from N render passes → 1; consecutive same-texture images collapse to 1 pass; the per-frame `create_buffer_init` for solids becomes a persistent, reused, growable buffer. `frame_stats()` proves the pass/draw-count drop. All output stays byte-exact; ordering preserved. Serves the "< 100 draw calls for 500-widget UI" target.
  - **Design:** Gradient coalescing via dynamic-offset uniform (stride = align_up(288, device.limits().min_uniform_buffer_offset_alignment), queried not hardcoded; binding 1 `has_dynamic_offset: true`, `min_binding_size: Some(288)`; one render pass, loop in existing order with `set_bind_group(0, &bg, &[i*stride])`). Texture coalescing: strictly adjacent same-texture-id draws merged; texture change starts new pass; NineSlice single-pass behaviour preserved. Persistent solid+gradient vertex buffers: `usage: VERTEX | COPY_DST`, grow next-power-of-two on overflow, `write_buffer` update, draw only `0..len`. Ordering invariant: clear → shadows → solid → gradients → textured; relative order within gradient set and same-texture runs preserved.
  - **Files:** `src/gpu/exec.rs`, `src/gpu/pipeline.rs` (gradient BG layout), `src/gpu/renderer.rs` (persistent buffer fields).
  - **Prerequisites:** R1 FrameStats + module split (renderer.rs <2000).
  - **Tests:** `two_gradients_one_pass` (frame_stats render_passes==1, correct color each region); `gradient_byte_exact_single` (offset-0 byte-identical to pre-batching); `same_texture_images_coalesce`; `persistent_buffer_reuse_stable`.
  - **Risk:** stride wrong → garbage 2nd-gradient; COPY_DST missing → validation error; only coalesce consecutive same-texture.
- [x] Instanced rendering (done 2026-06-03): `InstanceRect` (48-byte Pod) per-instance data; `InstancedRectPipeline` (instanced.wgsl — per-vertex UV quad + per-instance pos/size/color/corner-radius); `InstancedRectRenderer` (push/flush/clear, persistent instance buffer, one `draw_indexed(0..6, 0..n)` call); verified by `instanced_pipeline_compiles` + `instanced_renderer_renders_rects` GPU tests. Files: `src/gpu/instance.rs`, `src/shaders/instanced.wgsl`.
- [x] Anti-aliasing: MSAA (4x/8x configurable), edge-AA via SDF smoothstep in fragment shader, supersampling option for text (~100 SLOC)
  - **Goal:** `WgpuBackend` built with `RenderQuality { msaa: 4|8, .. }` renders all geometry with multisample coverage AA, resolving to the single-sample `Rgba8Unorm` readback target. `headless(w,h)` unchanged (msaa=1 → byte-identical, all existing tests green). Also corrects stale `supports_images`/`supports_blur` capability flags. (SDF edge-AA for rrects/circles/lines already exists via smoothstep. Supersampling for text deferred until DrawText/oxiui-text lands.)
  - **Design:** `RenderQuality::sample_count(&self)->u32`. `GpuContext` gains `sample_count:u32` + `msaa_view:Option<TextureView>`, `headless_with_sample_count(w,h,requested)` validates adapter support + falls back to 1. Screen pipelines (solid/gradient/textured/composite) take `sample_count` param; shadow mask pipeline (`solid_mask_pipeline`) stays count=1; blur stays count=1; composite matches screen count. All screen passes attach via `ctx.color_attachment()`. Capability overrides: `supports_images`→true, `supports_blur`→true.
  - **Files:** `src/quality.rs`, `src/gpu/device.rs`, `src/gpu/pipeline.rs`, `src/gpu/renderer.rs`, `src/gpu/shadow.rs`, `src/lib.rs`, `tests/headless_render_tests.rs`.
  - **Tests:** `msaa_smooths_diagonal_edge` (FillPath triangle, ≥1 edge pixel intermediate), `non_msaa_edge_is_hard` (msaa=1 → 0 or full only), `msaa_default_path_unchanged` (headless byte-identical), `msaa_unsupported_count_falls_back` (no panic). All existing tests pass (msaa=1 invariant).
  - **Risk:** sample-count/attachment mismatch → explicit screen=N / offscreen=1 split. MSAA texture not COPY_SRC → readback from resolved single-sample target.
- [x] Scissor/clip rectangles (done 2026-05-30): nested clip region stack, `set_scissor_rect` per draw call, clip region intersection for nested scroll areas (~80 SLOC) — (see wgpu foundation plan above)
  - **Landed:** `WgpuBackend::execute` segments the `DrawList` into per-clip draw runs by walking the existing tested `ClipStack` (intersection-on-push), then issues `set_scissor_rect` per segment before drawing its vertex range; out-of-bounds scissors are clamped and degenerate (fully off-screen) clips draw nothing. Verified by `clip_restricts_fill_to_clip_rect` and `nested_clip_intersects` headless tests.
- [x] Shadow rendering: BoxShadow via offscreen separable Gaussian blur (done 2026-06-02)
  - **Goal:** `WgpuBackend::execute` renders `DrawCommand::BoxShadow` as a blurred, offset, tinted rectangle behind content. `blur_radius==0` → crisp shadow; `blur_radius>0` → soft halo with monotone alpha falloff. Verified by pixel readback.
  - **Design:** Two offscreen `Rgba8Unorm` ping-pong textures at full viewport size, allocated per-`execute()` only when ≥1 shadow present (lazy). Per shadow: (a) clear ping to transparent; render white rect mask of `rect+offset` into ping using solid pipeline. (b) Horizontal blur ping→pong, vertical blur pong→ping via `blur.wgsl` (separable Gaussian; σ=max(blur_radius,1)/2; radius=ceil(3σ) clamped to MAX_BLUR_RADIUS=64; loop bounded by MAX). (c) Composite ping onto main `color_view` (LoadOp::Load, alpha blend). Pass-order restructure: (1) dedicated clear pass, (2) shadow composites, (3) solid pass (LoadOp::Load), (4) gradient passes, (5) textured passes. New `BlurPipeline` in `pipeline.rs`. New `gpu/shadow.rs` owns ping-pong allocation + blur dispatch.
  - **Files:** `src/shaders/blur.wgsl` (new), `src/gpu/shadow.rs` (new), `src/gpu/pipeline.rs`, `src/gpu/renderer.rs`, `src/gpu/buffer.rs`, `src/gpu.rs`, `src/lib.rs`.
  - **Prerequisites:** Offscreen render targets + ping-pong + separable Gaussian blur (built here).
  - **Tests:** `box_shadow_zero_blur_is_sharp`, `box_shadow_blur_halo_falloff`, `box_shadow_offset_translates`, `shadows_render_under_solids`.
  - **Risk:** Pass reordering changing existing output (regression suite guards this); blur cost at radius 64 bounded and fine for tests; note cached-target follow-up for perf.
  - **Note:** No spread/inset/corner-radius (DrawCommand::BoxShadow has none). DrawText deferred (needs oxiui-text glyph atlas).
- [x] Blur effects: backdrop-blur (done 2026-06-03): `DrawCommand::BackdropBlur { rect, blur_radius }` and `DrawList::push_backdrop_blur` added to oxiui-core; `BackdropBlurDraw` collected by `build_geometry`; `WgpuBackend::supports_backdrop_blur()` returns true; full execution path (copy-from-colour + separable Gaussian blur + write-back) wired as deferred render step using existing `BlurPipeline`/`CompositePipeline` infrastructure. Compute-shader blur alternative in `src/gpu/compute_blur.rs` + `src/shaders/blur_compute.wgsl`.
- [x] Gradient rendering: linear gradient (angle + color stops), radial gradient (center + radius + color stops) — GPU batched gradient pass (done 2026-06-03). Conic/sweep gradient and GPU-side gradient texture generation deferred.
- [x] Image rendering + NineSlice GPU rendering (textured pipeline) (done 2026-06-02)
  - **Goal:** `WgpuBackend::execute` renders `DrawCommand::Image` (RGBA blitted/scaled to `dest`, `Nearest`/`Bilinear` filter) and `DrawCommand::NineSlice` (9-patch: corners fixed, edges stretched one axis, center stretched both). Headless pixel-readback tests confirm sampled colours land at the right places.
  - **Design:** New `TexVertex { position:[f32;2], uv:[f32;2], tint:[f32;4] }` (32B) in `buffer.rs`. Helpers: `push_textured_quad` and `push_nine_slice_quads` (9 regions, UVs clamped to dest/img bounds). New `src/shaders/textured.wgsl` (same `Globals`+`pixel_to_ndc` preamble; texture+sampler in bind group 1, `Globals` in group 0). New `TexturedPipeline` in `pipeline.rs`. New `gpu/texture.rs`: `upload_image(device, queue, &ImageData, ImageFilter) -> (TextureView, Sampler)` (writes via `queue.write_texture`). Dispatch: replace `Image`/`NineSlice` wildcard in `build_geometry` with arms pushing `TexturedDraw`; add textured pass in `execute()` after solid pass (LoadOp::Load).
  - **Files:** `src/shaders/textured.wgsl` (new), `src/gpu/texture.rs` (new), `src/gpu/buffer.rs`, `src/gpu/pipeline.rs`, `src/gpu/renderer.rs`, `src/gpu.rs`, `src/lib.rs`.
  - **Prerequisites:** Textured pipeline + texture-upload path (built here).
  - **Tests:** `image_solid_fill_readback`, `image_nearest_vs_bilinear`, `nine_slice_corners_unscaled`, `tex_vertex_size_is_32`.
  - **Risk:** UV orientation / off-by-one in 9-slice region math; sRGB skew (upload as linear Rgba8Unorm); `build_geometry` lifetime change must not break existing paths.
  - **Note:** No fit/aspect mode (DrawCommand::Image has none); NineSlice insets are [top,right,bottom,left] in source pixels.
- [x] SDF text rendering (updated 2026-06-03): `TextBridge` (text feature, `src/text_bridge.rs`) — `expand_draw_text` shapes text via `oxiui-text::TextPipeline`, walks positioned glyphs, rasterizes via `GlyphAtlas` (LRU), converts greyscale coverage bitmaps to RGBA tinted by text colour, and pushes per-glyph `DrawCommand::Image` onto the output `DrawList` for rendering via the existing textured pipeline; no new GPU shaders required.
  **New 2026-06-03: `SdfTextPipeline`** (`src/sdf_text.rs`, behind `text` feature) — uploads [`oxitext_sdf::SdfTile`] glyph SDFs to an R8Unorm GPU texture atlas via a shelf packer and renders them with a dedicated WGSL SDF shader (`fs_main` uses `smoothstep` for sub-pixel-accurate coverage). Exposes `upload_glyph(device, queue, &SdfTile)` and `draw_text(device, rp, glyphs, color, vp_w, vp_h)`. Unit tests cover shelf packer allocation, atlas-full case, config defaults, AtlasEntry fields, and bytemuck Pod verification. Full GPU pipeline: R8Unorm atlas texture, bilinear sampler, premultiplied-alpha blend, WGSL vertex/fragment shaders.
- [x] Stencil buffer operations (done 2026-06-03): `StencilTarget` (`Depth24PlusStencil8` texture, resize support); `StencilWritePipeline` (write + clear variants with `Replace`/`Zero` stencil ops); `StencilClipState` (push/pop reference-value tracker, max depth 254); all verified by unit + GPU tests. File: `src/gpu/stencil.rs`.
- [x] Off-screen render targets (done 2026-06-03): `RenderTarget` with optional MSAA, `TEXTURE_BINDING | COPY_SRC` backing, `color_attachment()`, `readback_rgba()`, `resize()`, dirty/clean tracking; GPU tests verify zero-dim error, dirty flag, resize, readback size. File: `src/gpu/render_target.rs`.
- [x] GPU-driven bezier curve rendering: quadratic and cubic bezier fill/stroke via tessellation or SDF, path rendering for icons and custom shapes (~200 SLOC)
  - **Goal:** `DrawCommand::FillPath` renders geometrically correct filled regions for concave shapes, multi-sub-path shapes with holes (donut/glyph outlines), and disjoint shapes — honouring both `FillRule::NonZero` and `FillRule::EvenOdd`. A donut's hole is empty; a concave notch is empty; a shape where the two rules disagree renders differently per rule. Stroke tessellation is unchanged. Output stays `kind=0` triangles in the existing solid buffer/pass → no pass restructure, no GPU infra change, no core API change.
  - **Design:** New module `src/gpu/earcut.rs` exposing `pub fn triangulate(contours: &[Vec<[f32;2]>], fill_rule: FillRule) -> Vec<[[f32;2];3]>`. Winding + nesting via shoelace signed area + containment forest (point-in-polygon, nesting depth = chain length). Fill resolution: EvenOdd = even-depth contours are solid; NonZero = accumulate orientation sign, filled where winding≠0. Hole bridging: max-x vertex ray-cast to visible outer vertex, splice via coincident bridge edges. Ear clipping: O(n²), fan fallback on pathological input. `tessellate_fill` in `tessellator.rs` calls `earcut::triangulate`.
  - **Files:** `src/gpu/earcut.rs` (new), `src/gpu/tessellator.rs` (tessellate_fill body), `src/gpu.rs` (mod earcut), `src/gpu/renderer.rs` (GPU readback tests only).
  - **Tests:** Unit: convex square area, concave notch probe empty, donut hole probe empty, NonZero≠EvenOdd, degenerate no-panic. GPU: `fill_path_concave_notch_empty`, `fill_path_donut_hole_empty`, `fill_rule_evenodd_vs_nonzero`.
  - **Risk:** ear-clip robustness on degenerate input → fan fallback + unit tests. Single-contour case direct-ear-clipped (no nesting overhead).
- [x] Blend modes (done 2026-06-03): `BlendMode` enum (Normal/Multiply/Screen/Overlay/Darken/Lighten/Copy/Destination) added to oxiui-core with `push_blend_mode` DrawList helper; `blend_state_for_mode()` maps to `wgpu::BlendState`; `BlendPipelineSet` pre-compiles one solid pipeline per supported blend variant; `WgpuBackend::supports_blend_modes()` returns true. File: `src/gpu/blend.rs`. Note: Overlay/Darken/Lighten require per-pixel arithmetic not expressible in fixed-function blend — fall back to Normal.
- [x] HDR and wide-gamut support (done 2026-06-03): `SurfaceColorFormat` enum (Rgba8Unorm/Srgb, Bgra8Unorm/Srgb, Rgb10a2Unorm, Rgba16Float) with `is_hdr()`, `bits_per_channel()`, `expects_linear_light()`; `select_surface_format()` heuristic (prefer HDR when opted in, prefer sRGB for SDR); `HdrGpuContext` headless `Rgba16Float` context with `readback_f16()`; all paths tested. File: `src/gpu/hdr.rs`.
- [x] Frame pacing (done 2026-06-03): `FrameHistogram` (64-sample circular buffer, mean/min/max/p99 in µs); `FrameTimer` (GPU timestamp queries via `TIMESTAMP_QUERY` feature + CPU `Instant` fallback); `PresentModeRecommendation` (Fifo/Mailbox/Immediate) with `to_wgpu()`; adaptive heuristic based on p99 vs target. File: `src/gpu/frame_pacing.rs`.
- [x] Resize handling (done 2026-06-03): `GpuContext::resize(w,h)` recreates colour and MSAA textures in-place without rebuilding device/queue/pipelines; `WgpuBackend::resize(w,h)` delegates + updates globals uniform; zero-dim returns `UiError::Unsupported`. Tests: `resize_updates_surface_dimensions`, `resize_then_render_produces_correct_pixels`, `resize_zero_dimension_returns_error`.

## API Improvements
- [x] `RenderBackend` trait implementation (done 2026-05-30): implement the trait defined in `oxiui-core` for GPU rendering — (see wgpu foundation plan above)
  - **Landed:** `impl RenderBackend for WgpuBackend` in `src/gpu/renderer.rs` — `execute(&DrawList)` clears the target then replays `FillRect`/`FillCircle`/`PushClip`/`PopClip` in a single render pass (clip-leak-free); `surface_size()` returns the target size; `supports_text/images/blur/gradients/paths()` all return `false` this slice (TODO notes the deferred work). `readback_rgba()` copies texture→buffer, maps, and strips the 256-byte `COPY_BYTES_PER_ROW_ALIGNMENT` row padding to a tightly packed `w*h*4` buffer. 9 headless Metal tests pass with byte-exact pixel assertions.
- [x] Command buffer API: `DrawList` builder with `push_rect()`, `push_text()`, `push_image()`, `push_clip()`, `push_shadow()` methods
  - **Landed:** DrawList builder methods implemented in oxiui-core (push_rect, push_rounded_rect, push_circle, push_clip/pop_clip, push_shadow, push_image, push_nine_slice, push_text, push_stroke_rect, push_ellipse, push_line, push_line_aa, push_line_thick, push_line_dashed, push_path, push_stroke_path, push_gradient_linear, push_gradient_radial etc.).
- [x] Configurable quality settings: `RenderQuality { msaa: u32, shadow_quality: ShadowQuality, text_quality: TextQuality }` enum
  - **Landed:** `RenderQuality{msaa,shadow,text}` with Low/Balanced/High presets + `headless_with_quality` constructor wiring msaa=4/8 via MSAA pipeline path. `sample_count()` method validates and returns 1/4/8.
- [x] Resource handle API: `TextureHandle`, `ShaderHandle` with RAII cleanup, reference counting for shared resources
  - **Landed:** `TextureHandle`/`ShaderHandle` newtype wrappers over generation-checked `ResourceId` in `src/resource.rs`; `ResourceRegistry` with free-list recycling + generation bumping; RAII `TextureGuard`/`ShaderGuard` (via `Rc<RefCell<ResourceRegistry>>` on drop) with full unit tests.
- [x] Error handling: GPU-specific error variants (device lost, out-of-memory, shader compilation failure) mapped to `UiError`
  - **Landed:** `GpuErrorKind { DeviceLost, OutOfMemory, ShaderCompile, SurfaceLost }` in `src/error.rs` + `map_gpu_error(kind, detail) -> UiError` mapping (DeviceLost/OOM/SurfaceLost → `UiError::Render`; ShaderCompile → `UiError::Unsupported`); full coverage test `map_gpu_error_all_kinds`.

## Testing
- [x] Device initialization test (done 2026-06-03): `device_init_headless_succeeds_or_skips` in `tests/headless_render_tests.rs` — verifies `WgpuBackend::headless` returns `Ok` or `UiError::Unsupported`, never an unexpected error variant.
- [x] Render pipeline creation test (done 2026-06-03): `all_pipelines_compile_without_error` — constructing `WgpuBackend` triggers creation of all 5 pipelines; reaching the assertion means no WGSL compile panic.
- [x] Shader compilation tests (done 2026-06-03): `balanced_quality_pipelines_compile` — builds `headless_with_quality(balanced)` (MSAA sample_count=4 pipeline variant); all shader modules validated.
- [x] Texture atlas packing tests: pack 100 random-size rects, verify no overlap, verify atlas utilization > 70% (~80 SLOC)
  - **Landed:** `atlas_packs_100_rects_no_overlap` (100×4×4 inserts, exhaustive O(n²) overlap check) and `atlas_utilization_above_threshold` (1×1 tiles → ≥70%) in `src/atlas.rs::tests`.
- [x] Draw batching tests: 1000 rects with 5 different textures → verify batch count <= 5 (~40 SLOC)
  - **Landed:** `batcher_1000_rects_5_textures_le_5_batches` (unit, CPU-only) in `src/batch.rs::tests`; `many_solid_rects_coalesce_into_few_draws` (GPU, pixel-exact readback of alternating red/blue columns) in `tests/headless_render_tests.rs`.
- [x] Scissor rect tests (done 2026-06-03): `scissor_clip_restricts_draw_to_intersection` and `nested_scissor_clips_intersect` in `tests/headless_render_tests.rs` — nested clips produce correct intersection rects verified via pixel readback.
- [x] Snapshot / golden-image tests (done 2026-06-03): `tests/golden_image_tests.rs` renders 6 reference scenes (solid rects, rounded rects, circles+ellipses, gradient, image blit, clip+gradient) and compares against saved PNG baselines in `tests/testdata/golden/`; `OXIUI_UPDATE_GOLDENS=1` regenerates baselines; mean per-channel per-pixel diff threshold 2/255. Dependency: `png.workspace = true` in dev-dependencies.
- [x] Resize handling tests: resize from 800x600 to 1920x1080, verify surface reconfigured without panic (~30 SLOC)
  - **Landed:** `different_size_backends_work_independently` in `tests/headless_render_tests.rs` — two headless backends at 32×32 and 128×128 both execute a green fill and assert pixel readback at their respective centres. Window-surface resize deferred to the swap-chain surface milestone (src/surface.rs WIP).
- [x] Benchmark (done 2026-06-03): `benches/rrect_bench.rs` using criterion measures `execute_10k_rrects` (10 000 rounded rects on 1920×1080 headless backend) and `build_draw_list_10k` (CPU-only DrawList construction); gracefully skips when no GPU available. `criterion.workspace = true` in dev-dependencies; `[[bench]]` entry in Cargo.toml.

## Performance
- [x] Renderer module split + FrameStats instrumentation (planned 2026-06-03)
  - **Goal:** `renderer.rs` drops below 2000 lines (currently 1828) by extracting cohesive seams into sibling gpu modules, zero behaviour change (entire existing suite byte-exact green). New `FrameStats { draw_calls: u32, render_passes: u32 }` exposed via `WgpuBackend::frame_stats() -> FrameStats`, turning the "can't count draws" comment into a real assertion.
  - **Design:** Extract `gpu/geometry.rs` (build_geometry + pure-CPU helpers: cmd_bounds, rect_from_points, rects_intersect, scissor_to_rect, stroke/dashed emitters, gradient-draw builders, DrawSegment/GradientDraw structs) and `gpu/exec.rs` (run_solid_pass/run_gradient_pass/run_textured_pass); `execute()` stays as short orchestrator. Use `splitrs` per policy; manually verify `super::` paths in ~600-line test module. FrameStats: struct in exec.rs or buffer.rs, `last_frame_stats` field, counters after dead-segment filters, include shadow passes.
  - **Files:** `src/gpu/renderer.rs`, new `src/gpu/geometry.rs`, new `src/gpu/exec.rs`, `src/gpu.rs`, `src/lib.rs`, `tests/headless_render_tests.rs`.
  - **Prerequisites:** none.
  - **Tests:** existing suite unchanged+green; update `many_solid_rects_coalesce_into_few_draws` to assert `frame_stats().draw_calls ≤ 2`; add `frame_stats_counts_solid_draws`.
  - **Risk:** splitrs breaking super:: in test module → manual verify + --no-run compile gate.
- [x] Draw call batching: target < 100 draw calls for a typical 500-widget UI
- [x] Texture atlas fragmentation monitoring (done 2026-06-03): `TextureAtlas::is_fragmented(threshold)`, `defrag()` (O(n log n) in-place layout rebuild, returns relocations), `defrag_if_fragmented(threshold)`, `allocation_count()`; tested by `atlas_fragmentation_detected_and_defrag_works`, `atlas_is_fragmented_threshold`, `atlas_defrag_if_fragmented_skips_when_healthy`.
- [x] GPU upload streaming (done 2026-06-03): `RingBuffer` (`VERTEX|INDEX|COPY_DST`, next-power-of-two growth, `reset()` per frame, `upload()` aligns to caller-supplied alignment); `RingAllocation { offset, size }`; `RingBufferStats`; CPU unit tests verify `align_up`, stats default, allocation fields. File: `src/gpu/ring_buffer.rs`.
- [x] Render layer caching (done 2026-06-03): `LayerCache` (pool of `RenderTarget`s keyed by `u64` layer_id); `begin_frame()` evicts idle layers; `get_or_create()` with LRU eviction at capacity; `invalidate(id)` / `invalidate_all()`; `remove()` / `clear()`; 8 GPU tests (empty, get/create, invalidate, idle eviction, LRU at capacity, clear, invalid id, invalidate_all). File: `src/gpu/layer_cache.rs`.
- [x] Compute shader for blur (done 2026-06-03): `ComputeBlurPipeline` using `blur_compute.wgsl` (16×16 workgroup, separable Gaussian, horizontal/vertical passes via `horizontal:u32` uniform, `rgba8unorm` storage write); `dispatch_pass()` for one pass; tested by `compute_blur_pipeline_compiles_or_unsupported`. Files: `src/gpu/compute_blur.rs`, `src/shaders/blur_compute.wgsl`.
- [x] Visibility culling: skip draw commands for widgets outside the viewport clip rect before submitting to GPU (done 2026-06-03: cmd_bounds/rects_intersect helpers + scissor intersection gate in build_geometry)
- [x] Pipeline state caching: avoid redundant `set_pipeline`/`set_bind_group` calls when state is unchanged between draws (done 2026-06-03: last_pipeline_ptr/last_globals_bg_ptr dedup locals in solid pass execute())

## Integration
- [x] `oxiui-core` integration: `WgpuBackend` implements `RenderBackend` trait; accepts `DrawList` from layout/paint phase (done 2026-05-30)
- [x] `oxiui-text` integration (done 2026-06-03): `TextBridge` in `src/text_bridge.rs` (behind `text` feature) bridges `oxiui-text::TextPipeline` + `GlyphAtlas` to the wgpu textured pipeline; `expand_draw_text` rasterizes shaped glyphs and pushes per-glyph `DrawCommand::Image` quads; unit tests cover greyscale→RGBA conversion and atlas utilization.
- [x] `oxiui-theme` integration (done 2026-06-03): `src/theme_bridge.rs` extended with `push_border_spec` (uniform solid/dashed/dotted/double border from `BorderSpec`) and `push_border_specs` (per-side border with fast uniform path); `ShadowSpec`/`push_shadow_spec`/elevation helpers already complete; 9 new unit tests added. Border/shadow/gradient design-token to `DrawList` bridge complete.
- [x] `oxiui-render-soft` integration: shared `DrawList` / `DrawCommand` format in `oxiui-core::paint` consumed by both `WgpuBackend` and `SoftBackend` (done 2026-05-30)
- [ ] `oxiui-web` integration: wgpu WebGPU backend for browser rendering, shared shader code
  **BLOCKED: wgpu surface + adapter initialization requires wasm32/WebGPU target and a live browser canvas (wasm-bindgen window handle). Cannot be tested or compiled on native targets without a full wasm32 toolchain + web-sys/js-sys context. Deferred to a dedicated wasm32 build pass.**
- [x] `oxiui-accessibility` integration (done 2026-06-03): `src/a11y_bridge.rs` (behind `accessibility` feature) provides `push_focus_ring` (renders a `FocusRing` spec from `oxiui-accessibility` as a rounded `StrokePath` at the widget boundary + outset offset) and `is_high_contrast_active()` (reads `OXIUI_HIGH_CONTRAST` env var via `OsA11yPrefs::query()`). 5 unit tests cover sharp/rounded rings, degenerate rect clamping, and high-contrast query.
- [x] COOLJAPAN ecosystem (verified 2026-06-03): all 7 shaders are WGSL (solid/gradient/textured/instanced/blur/blur_compute/composite — no GLSL/HLSL/SPIR-V); no stb_image / image / flate2 / zstd / zip / bincode in dependencies; GPU Gaussian blur is native WGSL (no OxiFFT needed — GPU > CPU FFT for this use-case; OxiFFT listed as available workspace dep if future CPU-side convolution needed); texture compression via Pure Rust `png` decoder (dev-dep only, for golden image tests); no oxiarc usage required (no binary assets bundled in this crate).

## Proposed follow-ups
- Deferred: GPU device/pipeline/WGSL/SDF/MSAA/shadow/blur/gradient/image/stencil/offscreen/instanced — GPU-hardware-blocked. CPU foundations landed in Slice A.