Skip to content

Drawer and batching

Tick UI records draw commands through the Drawer during DoUI and replays them into a Unity CommandBuffer when the render-pipeline integration calls RecordInto. This page explains the command-list model, how instanced batching works, and the planned draw-layer pass.

The command list

Drawer.m_commands is a List<DrawCommand> that fills during DoUI and drains in RecordInto. Each DrawCommand is a pooled object carrying the per-call parameters (rect, colour, transform, mesh reference, and so on).

User code does not interact with the command list directly. Calls into ui.FillRounded, ui.Text, and friends translate through the active panel's DrawCommands list, which EndFrame then flushes into Drawer.m_commands in panel-layer order. For the flush ordering, refer to Frame lifecycle.

Per-shape command types

DrawCommand.DrawType enumerates every shape Tick UI can draw:

  • Rectangle, Shadow, Frosted: shape primitives drawn over the unit quad.
  • Polygon, Line, Segment: vertex-driven primitives.
  • Text, Texture, Texture9Slice, TextureByName: textured draws.
  • Shape, CustomShape: SDF shapes (the built-in catalogue and user shaders).

The GPU scissor is not a DrawType. Each command carries an optional ClipRect, and the batch loop applies it through CommandBuffer.EnableScissorRect / DisableScissorRect when the clip rect changes between commands.

Each type has a corresponding Handle* method in Drawer.cs that configures a MaterialPropertyBlock and issues a target.DrawMesh call, except the batched types (see below) which draw through their batchers.

Materials

Drawer builds one Material per shape type in its constructor. Each shape's material wraps a shader fetched through the resource provider chain, so user-supplied providers can override pipeline specific shader variants. For the chain, refer to Resource providers.

MaterialPropertyBlock per command

Each passthrough Handle* method configures a per-call MaterialPropertyBlock with the rect's _Scale, _Offset, _Color, _Radius, and similar values, then issues target.DrawMesh. Passthrough shapes produce one draw call each.

The batched shape types (rectangle, segment, and SDF shape) do not go through a per-call Handle* method. They accumulate into their batchers and draw as instanced batches — see Instancing below.

Frosted glass and the grab-and-blit

HandleDrawFrosted is the most expensive shape type. The pipeline:

  1. Allocate a temporary RenderTexture sized to the configured render-target size (ResolveTargetSize, which falls back to Screen.width × Screen.height when no explicit size is set).
  2. Blit the current target into the temp RT.
  3. Restore the original target (the RenderTargetIdentifier set through Drawer.SetRenderTarget).
  4. Draw the frosted shape, sampling the temp RT in the shader.
  5. Release the temp RT.

The blit-per-call lets each frosted shape see everything drawn before it.

The temp RT is sized to the render target the drawer was given, so projects that render to an off-screen target get a correctly sized grab as long as that size is passed through SetRenderTarget. Refer to Render pipelines.

Recording cost

CPU cost on the recording side is the limiting factor on typical hardware. Instancing collapses runs of rectangles, segments, and SDF shapes into a handful of draw calls, but passthrough shapes (text, textures, frosted, line, polygon) still record one DrawMesh each. Profiler markers attribute the cost. Refer to Profiler markers for the full list.

Common hot spots include:

  • Text: cached after first use, but the first frame of new text pays the mesh-build cost.
  • Frosted glass: per-call temp RT allocation, blit, and shader sample.
  • Repeated layout pushes: rect-stack push and pop overhead in schemas with hundreds of elements.

Instancing

BuildBatchCommands runs before recording and groups consecutive same-type commands into batches. The batched types draw through CommandBuffer.DrawMeshInstancedProcedural from a per-batcher structured buffer of instance data:

  • Batches: rectangle (RectangleBatcher), segment (SegmentBatcher), and SDF shape (ShapeBatcher).
  • Does not batch (passthrough, one DrawMesh each): shadow, frosted (per-call grab-and-blit), texture and 9-slice (per-call sampler), text (per-call mesh), line, polygon, and custom shape.

A batch ends when the command type changes or the active clip rect changes. At record time, RecordBatchCommands walks the batch list, toggling the GPU scissor as the clip rect changes and dispatching each batch through its batcher or each passthrough command through its Handle* method.

Draw layers (planned)

The planned layer-bucket system lets opted-in widgets push commands into per-layer buckets within a panel:

ui.PushLayer(Layer.Background);
    ui.FillRounded(...);
ui.PopLayer();

ui.PushLayer(Layer.Content);
    ui.Text(...);
ui.PopLayer();

At flush, layers drain in fixed order (background, border, content, overlay). Within a layer, commands stay in submission order, so the instancing batcher receives long runs of the same shape type.

Pitfalls

Per-frame draw-call count is high. : This is expected. Profile under realistic load before optimising. Many panel-heavy UIs run fine on modern hardware. Refer to the Performance guide.

Frosted glass is expensive. : Each frosted shape costs one full-target blit plus one multi-tap shader sample. Use sparingly; group frosted regions where possible.

Custom shaders pink-render after a provider change. : The custom provider has shader entries that miss some shapes. Refer to Resource providers.

Additional resources