Skip to content

RUNE-304: Drawing Proposed

Updated2026-08-23

Summary

Rune records drawing as one typed, 8-byte-aligned command stream plus one typed data arena per RenderNodeSnapshot. DATA_PATH version 2 can carry original bytecode plus source-aware arc-length metrics. CMD_STROKE_TRIMMED_PATH consumes an anonymous measured path to reveal a normalized stroke interval with Windows Composition-compatible wrapping while fill and clip keep the complete path. Retained RuneSvg assets participate in the same command stream through CMD_DRAW_SVG.

Motivation

The drawing model must keep snapshots compact, replayable by linear scan, and self-contained enough for render-thread traversal. At the same time, Rune needs stable paint identity across snapshots, explicit transform state, animated path geometry, child composition that participates in structural bounds and replay uniformly, and a backend where solid, gradient, image, text, and double-rrect all flow through one replay pipeline. Encoding children as CMD_DRAW_CHILD, keeping paint in CMD_DATA, and moving path payloads into typed DATA_PATH records preserves that uniformity without forcing parent command-buffer rebuilds whenever only a child snapshot changes, while ResourceSlot gives brush and path realizations retained lifetimes instead of per-frame cache keys.

Design

Command Header and Op Set

Every record starts with the same header:

cpp
struct CmdHeader {
    uint8_t op;
    uint8_t sub_type;
    uint16_t size;
};

op selects the record family, sub_type refines families such as CMD_DATA, and size advances linear replay across fixed-size and variable-size records alike.

cpp
enum RuneCommandOp : uint8_t {
    CMD_DATA,
    CMD_SET_OFFSET, CMD_SET_OPACITY,
    CMD_SAVE_TRANSFORM, CMD_RESTORE_TRANSFORM,
    CMD_SET_TRANSFORM, CMD_CONCAT_TRANSFORM,
    CMD_CONCAT_SCALE, CMD_CONCAT_ROTATION, CMD_SET_TRANSFORM_ORIGIN,
    CMD_DRAW_RECT, CMD_DRAW_RRECT, CMD_DRAW_DOUBLE_RRECT, CMD_DRAW_ELLIPSE, CMD_DRAW_ARC, CMD_DRAW_PATH,
    CMD_STROKE_LINE, CMD_STROKE_PATH, CMD_STROKE_TRIMMED_PATH,
    CMD_DRAW_NINE_SLICE, CMD_DRAW_SHAPED_TEXT, CMD_DRAW_CHILD,
    CMD_PUSH_CLIP_RECT, CMD_PUSH_CLIP_RRECT, CMD_PUSH_CLIP_PATH, CMD_POP_CLIP,
    CMD_DRAW_BOX_SHADOW, CMD_DRAW_BOX_SHADOW4, CMD_DRAW_SVG,
};

That is 29 ops total: 1 data op, 2 property ops, 7 transform ops, 9 shape/effect/asset draw ops, 3 stroke ops, 3 other draw ops, and 4 clip ops. Consumers must use symbolic names rather than command numbers.

CMD_DATA and Sub Types

CMD_DATA is the shared carrier for reusable brush descriptors:

cpp
enum RuneDataType : uint8_t {
    DATA_BRUSH_SOLID,
    DATA_BRUSH_LINEAR_GRADIENT,
    DATA_BRUSH_RADIAL_GRADIENT,
    DATA_BRUSH_CONIC_GRADIENT,
    DATA_BRUSH_IMAGE,
    DATA_PATH_PROGRAM, // legacy inline path subtype, no longer recorded
};
  • DATA_BRUSH_* stores a brush descriptor plus the ResourceSlot* that replay configures.
  • Paths now live in data_buffer as DATA_PATH, addressed by Path offsets instead of CMD_DATA subtypes.

This keeps the op set small while preserving one linear replay path for executable commands. sub_type is therefore semantic, not padding.

Property and Transform Ops

Property and transform ops live in the same buffer as draw data:

cpp
struct CmdSetOffset   { CmdHeader header; Value x, y; };
struct CmdSetOpacity  { CmdHeader header; Value alpha; };
struct CmdSaveTransform    { CmdHeader header; };
struct CmdRestoreTransform { CmdHeader header; };
struct CmdSetTransform     { CmdHeader header; Value m11, m12, m21, m22, m31, m32; };
struct CmdConcatTransform  { CmdHeader header; Value m11, m12, m21, m22, m31, m32; };
struct CmdConcatScale      { CmdHeader header; Value sx, sy; };
struct CmdConcatRotation   { CmdHeader header; Value degrees; };
struct CmdSetTransformOrigin { CmdHeader header; Value x, y; uint8_t mode; };

Transform replay is explicit command-stream state:

  • CMD_SAVE_TRANSFORM pushes the active matrix and transform origin.
  • CMD_RESTORE_TRANSFORM pops and restores the previous matrix and transform origin. If no saved state exists, replay treats it as a no-op so inherited parent transform state is not discarded.
  • CMD_SET_TRANSFORM replaces the active 3x2 matrix.
  • CMD_CONCAT_TRANSFORM appends a raw 3x2 matrix.
  • CMD_CONCAT_SCALE and CMD_CONCAT_ROTATION append semantic transforms around the current transform origin.
  • CMD_SET_TRANSFORM_ORIGIN sets the origin used by later semantic concat commands. Origin mode can be absolute local pixels or relative to untransformed layout/content bounds; if neither layout nor local bounds exist, relative origin falls back to the local origin (0,0).
  • All numeric fields stay in Value, so static and animated values share the same decode path.

Replay mirrors the active matrix to ID2D1DeviceContext::SetTransform. Local content bounds remain untransformed; snapshot visual bounds are conservatively transformed for capture, projection, and transient layer allocation and account for command offset plus save/restore-scoped transform state.

Fill and Stroke Commands

All shape and stroke commands carry geometry plus typed handles. Brush is a uint32_t cmd_buffer offset to a brush CMD_DATA record. Path is a uint32_t data_buffer offset to a DATA_PATH record that points to path bytecode and the slot used for path-geometry caching.

cpp
typedef struct Path { uint32_t _id; } Path;
typedef struct Brush { uint32_t _id; } Brush;

struct CmdDrawRect        { CmdHeader header; Value x, y, w, h; Brush brush; };
struct CmdDrawRRect       { CmdHeader header; Value x, y, w, h, radius; Brush brush; };
struct CmdDrawDoubleRRect { CmdHeader header; Value x, y, w, h, radius, border_width; Brush fill_brush; Brush border_brush; };
struct CmdDrawBoxShadow   { CmdHeader header; Value x, y, w, h, radius, offset_x, offset_y, blur_radius, spread, c0, c1, c2, alpha; uint8_t color_space; };
struct CmdDrawBoxShadow4  { CmdHeader header; Value x, y, w, h, top_left, top_right, bottom_right, bottom_left, offset_x, offset_y, blur_radius, spread, c0, c1, c2, alpha; uint8_t color_space; };
struct CmdDrawEllipse     { CmdHeader header; Value cx, cy, rx, ry; Brush brush; };
struct CmdDrawArc         { CmdHeader header; Value cx, cy, rx, ry, start_angle, sweep_angle; Brush brush; };
struct CmdDrawPath        { CmdHeader header; Path path; Brush brush; };

struct CmdStrokeLine {
    CmdHeader header;
    Value x1, y1, x2, y2, width;
    Brush brush;
    uint8_t cap;
};

struct CmdStrokePath {
    CmdHeader header; // size includes inline dash values
    Path path;
    Value width;
    float miter_limit;
    float dash_offset;
    Brush brush;
    uint8_t cap, join, dash_count;
    // float dash_values[dash_count] follows
};

struct CmdStrokeTrimmedPath {
    CmdHeader header;
    Path path;
    Value width, trim_start, trim_end, trim_offset;
    float miter_limit;
    Brush brush;
    uint8_t cap, join;
    uint16_t reserved;
};

Dash values stay inline in CmdStrokePath because they are small command-local payloads rather than reusable resources. A null dash pointer records a solid stroke even if a non-zero dash count is supplied.

CmdStrokeTrimmedPath is a fixed 36-byte command. Its recording API atomically appends a slot-backed anonymous measured DATA_PATH and the command; width/start/end/offset remain live Values. Trim is stroke-only in V1: fill and clip continue using complete immutable geometry, and the existing dashed stroke command is unchanged.

Trim normalization sanitizes non-finite values to zero, clamps start/end to [0,1], classifies exact empty/full before offset, and applies positive-modulo offset. end < start is a forward seam-wrapped interval, matching Windows Composition. Lottie instead sorts start/end, so importers must translate that semantic difference. start=1,end=0 is empty.

CmdDrawArc records a filled pie-sector command. Angles are radians, 0 points right, positive sweep is clockwise in the D2D y-down coordinate space, and a full sweep fills the equivalent ellipse.

D2D replay maps path fills to FillGeometry, stroke paths to DrawGeometry, stroke lines to DrawLine, and arcs to command-local geometry plus FillGeometry. Stroke commands use stroked visual bounds for relative gradient and image brush mapping. ID2D1StrokeStyle objects are command-local in the first implementation; style caching is deferred until profiling shows repeated styled strokes are hot.

Other Draw, Text, Child, and Clip Commands

cpp
struct CmdDrawNineSlice  { CmdHeader header; Value x, y, w, h; NineSlice* nine_slice; };
struct CmdDrawShapedText { CmdHeader header; ShapedText* shaped; uint32_t glyph_start, glyph_count; Value x, y; Brush brush; };
struct CmdDrawChild      { CmdHeader header; uint16_t child_slot; };
struct CmdDrawSvg        { CmdHeader header; RuneSvg* svg; Value x, y, w, h; uint8_t preserve_aspect; };
struct CmdPushClipRect   { CmdHeader header; Value x, y, w, h; };
struct CmdPushClipRRect  { CmdHeader header; Value x, y, w, h, radius; };
struct CmdPushClipPath   { CmdHeader header; Path path; };
struct CmdPopClip        { CmdHeader header; };

CmdDrawChild is recorded inline beside every other draw command. Its child_slot indexes into the owning snapshot's child_slots[] array. It participates in structural snapshot bounds and child-boundary replay, with contribution derived from the referenced child snapshot's projected bounds rather than stored in the command record.

CmdDrawNineSlice references an immutable NineSlice resource created from a Image, source rect, source insets, and interpolation mode. D2D replay resolves the image through the per-device bitmap cache, splits the source and destination into a 3x3 grid, and draws non-empty cells with DrawBitmap. V1 stretches edges and center only. If the destination is smaller than the fixed insets, replay scales destination insets proportionally so the center never becomes negative and cells do not overlap.

CmdDrawSvg references an immutable RuneSvg asset. The asset addrefs a finished root snapshot plus viewBox/intrinsic-size metadata and can be shared across draw sites. Replay maps the SVG viewBox into the live destination rectangle, resolves the retained snapshot at the current frame time, and replays it recursively. Bounds and value analysis recognize CMD_DRAW_SVG, so animated destinations and animated values inside the retained snapshot remain part of ordinary snapshot analysis. The preserve_aspect field is retained, but V1 replay stretches to the destination rectangle.

Clip replay uses a mixed D2D clip stack. Rectangular clips use PushAxisAlignedClip / PopAxisAlignedClip; rounded-rectangle and path clips use PushLayer with a geometry mask and PopLayer. CMD_POP_CLIP dispatches to the correct pop based on the stack entry, and end-of-snapshot cleanup unwinds any unbalanced clips. Path clips use Winding/non-zero fill in V1.

Replay stays linear until it encounters CMD_DRAW_CHILD:

  1. Decode the command header.
  2. Read snapshot->child_slots[child_slot] from the owning snapshot.
  3. Inspect the child snapshot's opacity to choose direct replay or D2D PushLayer group-opacity compositing. blend_mode and cache_hint are retained for future compositing/cache features but do not affect D2D replay in V1.
  4. Recurse into that child snapshot using the selected path.

This keeps child composition in the command model instead of a parallel child tree structure embedded in the snapshot.

Brush Data Records

All paint lives in CMD_DATA records which carry the ResourceSlot* configured during replay. Brush recording functions (rune_node_brush_solid, etc.) return a Brush handle — the cmd_buffer offset of the brush CMD_DATA. Draw commands store this Brush offset and the D2D backend resolves it back to the slot during replay:

cpp
struct BrushSolid {
    CmdHeader header; // CMD_DATA + DATA_BRUSH_SOLID
    ResourceSlot* slot;
    uint8_t color_space;
    Value c0, c1, c2, alpha;
};

struct BrushLinearGradient {
    CmdHeader header; // CMD_DATA + DATA_BRUSH_LINEAR_GRADIENT
    ResourceSlot* slot;
    uint8_t color_space, gradient_mode, tile_mode, stop_count;
    Value start_x, start_y, end_x, end_y;
    // GradientStopRV stops[stop_count] follows
};

struct BrushRadialGradient {
    CmdHeader header; // CMD_DATA + DATA_BRUSH_RADIAL_GRADIENT
    ResourceSlot* slot;
    uint8_t color_space, gradient_mode, tile_mode, stop_count;
    Value cx, cy, rx, ry, focal_x, focal_y;
    // GradientStopRV stops[stop_count] follows
};

struct BrushConicGradient {
    CmdHeader header; // CMD_DATA + DATA_BRUSH_CONIC_GRADIENT
    ResourceSlot* slot;
    uint8_t color_space, gradient_mode, tile_mode, stop_count;
    Value cx, cy, start_angle, end_angle;
    // GradientStopRV stops[stop_count] follows
};

struct BrushImage {
    CmdHeader header; // CMD_DATA + DATA_BRUSH_IMAGE
    ResourceSlot* slot;
    Image* image;
    uint8_t fill_mode, interpolation;
    Value src_x, src_y, src_w, src_h, offset_x, offset_y;
};

For image brushes, offset_x/offset_y are target-space offsets applied only to repeating axes: Tile applies both, TileX applies offset_x only, TileY applies offset_y only, and Stretch ignores both.

One brush format serves fill, stroke, shaped text, and both inputs of CMD_DRAW_DOUBLE_RRECT. Gradient stops remain inline so one slot-configured brush descriptor can be reused by many draws while preserving stable brush identity across snapshots and frames.

Typed Path Data

Path segments are stored as 4-byte-opcode bytecode inside DATA_PATH version 2. Its fixed header remains 64 bytes and measured payload is:

text
bytecode
MeasuredPathSegment[segment_count]  // bytecode offset + segment start point
PathMetricKnot[knot_count]          // cumulative length + segment index + source t

Each measured record is 4-byte component aligned and 8-byte record aligned. The old sample count is now knot_count; the old reserved 32-bit field is segment_count. Draw-only CreatePath uses zero counts. SamplePath records a slotless anonymous measured path. Trimmed stroke records a separate slot-backed anonymous measured path; V1 intentionally has no public shared measured-Path handle.

Managed measurement retains source t while adaptively flattening curves at the default 0.25 DIP tolerance. Lookup binary-searches cumulative knots and evaluates the original Line/Quad/Cubic/Close instruction, so sampling and trim boundaries share one curve-faithful metric model. Exact boundary ties choose the next positive-length segment at t=0; Close contributes length; all-zero segment-bearing paths sample their initial point; move-only, dynamic, and multiple-figure measured paths are rejected.

Direct2D exposes complete-path measurement but no arc-length extraction. Partial trimmed replay therefore creates a new open ID2D1PathGeometry, copies complete interior source segments, and de Casteljau-splits boundary curves. A wrapped closed contour remains one figure across the seam; a wrapped open path uses tail and head figures. Full trim reuses the slot-cached complete geometry, and partial geometry never replaces it.

Recorded bounds use the complete source path expanded by the maximum animated width and cap/join factor. At replay, relative brush coordinates use complete geometry widened by the current resolved stroke style, never the partial trim geometry. Sampled X/Y descriptor ranges likewise use complete conservative source bounds rather than knot coordinates. This keeps structural bounds, composited layers, gradients, and follower alignment stable throughout reveal.

Sample and trim recording preflight the aligned measured path and complete pending DATA_EXPRESSION before mutation. Sampling additionally reserves expression output slots and builder bytes; trim additionally reserves the fixed command and resource ref. Malformed input returns invalid argument, dynamic measured geometry returns unsupported, and capacity returns failed without a success-shaped fallback.

Double Rounded Rect

CMD_DRAW_DOUBLE_RRECT remains Rune's dedicated border primitive:

cpp
struct CmdDrawDoubleRRect {
    CmdHeader header;
    Value x, y, w, h, radius, border_width;
    Brush fill_brush;
    Brush border_brush;
};

Replay resolves both brush slots to D2D brushes. The D2D backend builds outer and inner rounded-rectangle geometries; the border is an alternate-fill geometry group and the fill is drawn in the inner geometry. The rendering model remains:

  • outer shape = (x, y, w, h, radius)
  • inner shape = inset by border_width, with inner_radius = max(radius - border_width, 0)
  • result = fill interior + border ring, never a centerline stroke

Box Shadow

CMD_DRAW_BOX_SHADOW and CMD_DRAW_BOX_SHADOW4 are Rune's dedicated CSS-like outer shadow primitives for rounded rectangles:

cpp
struct CmdDrawBoxShadow {
    CmdHeader header;
    Value x, y, w, h, radius;
    Value offset_x, offset_y;
    Value blur_radius, spread;
    Value c0, c1, c2, alpha;
    uint8_t color_space;
};

struct CmdDrawBoxShadow4 {
    CmdHeader header;
    Value x, y, w, h;
    Value top_left, top_right, bottom_right, bottom_left;
    Value offset_x, offset_y;
    Value blur_radius, spread;
    Value c0, c1, c2, alpha;
    uint8_t color_space;
};

The rendering model is:

  • scalar caster shape = (x, y, w, h, radius)
  • per-corner caster shape = (x, y, w, h, top_left, top_right, bottom_right, bottom_left)
  • shadow shape = caster translated by (offset_x, offset_y) and inflated by spread
  • output bounds = shadow shape inflated by blur_radius
  • negative spread is allowed; degenerate shadow geometry is skipped

V1 uses single-pass analytical SDF effects with scene-space constants, not a temporary render target blur. The shader applies a Gaussian-like finite falloff to the shadow SDF and multiplies it by an outside-caster mask, so the outer shadow is clipped out of the original caster interior. Shadow color is direct solid color values (color_space, c0, c1, c2, alpha) rather than a Brush; multiple shadows are recorded as multiple commands. CMD_DRAW_BOX_SHADOW4 uses top-left, top-right, bottom-right, bottom-left radius order and normalizes adjacent radii to fit the box.

Command Buffer Encoding and 1 MB Limit

A RenderNodeSnapshot owns one contiguous cmd_buffer plus one contiguous data_buffer. Property ops, CMD_DATA records, and draw or clip commands live in cmd_buffer; immutable DATA_PATH records live in data_buffer. Every record is 8-byte aligned so pointer-bearing payloads remain naturally aligned on 64-bit targets.

CommandBuffer::MAX_SIZE is 0x100000, so one snapshot may use up to 1 MB of cmd_buffer space. Brush remains a uint32_t cmd_buffer offset. Path is a typed uint32_t data_buffer offset. CmdHeader::size still stays uint16_t, so single commands remain capped at <= 0xFFFF bytes and push_with_extra() rejects any larger aligned payload, while DataHeader::size is 32-bit so a single DATA_PATH record is no longer constrained by the command header.

Variable-size brush payloads such as gradient stops and stroke dashes still depend only on CmdHeader::size, while path bytecode, measured segments, and knots use the data arena's 32-bit DataHeader::size.

D2D Brush Pipeline

Rune realizes brush CMD_DATA into slot-owned D2D resources. Solid brushes store a resolved color descriptor and lazily create ID2D1SolidColorBrush for ordinary drawing; shader-input paths can request a lazy Flood-effect image. Gradient brushes use effect outputs as their image source, and image brushes use a cached ID2D1Bitmap1 from the referenced Image.

  • normal shapes: solid brushes feed FillRectangle, FillGeometry, glyph drawing, and related draw paths as ID2D1SolidColorBrush; gradient/image brushes feed those calls as ID2D1ImageBrush
  • double-rrect: fill and border resolve to ordinary D2D brushes and draw disjoint rounded-rectangle geometry
  • box shadow: reusable custom SDF effects draw solid-color scalar and per-corner rounded-rectangle shadows with finite bounds
  • image fill: source image and sampling parameters resolve through the same slot/resource path as other brushes

ResourceSlot owns the D2D realization (direct solid brush, effect/image brush resources for brushes, or ID2D1PathGeometry for paths). Replay reads brush slots from resolved CMD_DATA and path slots from resolved DATA_PATH, checks ISlotResource type-tag compatibility, and uses hashes to skip rebuilds when the resolved configuration is unchanged. Solid slots track separate applied hashes for direct-brush and Flood-image realizations because both are lazy. If the slot type changes, replay destroys the old realization and creates a new one.

API Surface

Enums

cpp
enum RuneCommandOp : uint8_t {
    CMD_DATA,
    CMD_SET_OFFSET, CMD_SET_OPACITY,
    CMD_SAVE_TRANSFORM, CMD_RESTORE_TRANSFORM,
    CMD_SET_TRANSFORM, CMD_CONCAT_TRANSFORM,
    CMD_CONCAT_SCALE, CMD_CONCAT_ROTATION, CMD_SET_TRANSFORM_ORIGIN,
    CMD_DRAW_RECT, CMD_DRAW_RRECT, CMD_DRAW_DOUBLE_RRECT, CMD_DRAW_ELLIPSE, CMD_DRAW_ARC, CMD_DRAW_PATH,
    CMD_STROKE_LINE, CMD_STROKE_PATH, CMD_STROKE_TRIMMED_PATH,
    CMD_DRAW_NINE_SLICE, CMD_DRAW_SHAPED_TEXT, CMD_DRAW_CHILD,
    CMD_PUSH_CLIP_RECT, CMD_PUSH_CLIP_RRECT, CMD_PUSH_CLIP_PATH, CMD_POP_CLIP,
    CMD_DRAW_BOX_SHADOW,
};

enum RuneDataType : uint8_t {
    DATA_BRUSH_SOLID,
    DATA_BRUSH_LINEAR_GRADIENT,
    DATA_BRUSH_RADIAL_GRADIENT,
    DATA_BRUSH_CONIC_GRADIENT,
    DATA_BRUSH_IMAGE,
    DATA_PATH_PROGRAM, // legacy inline path subtype
};
c
typedef enum {
    RUNE_COLOR_SRGB,
    RUNE_COLOR_LINEAR_RGB,
    RUNE_COLOR_OKLAB,
    RUNE_COLOR_OKLCH,
    RUNE_COLOR_DISPLAY_P3,
} RuneColorSpace;

typedef enum { RUNE_GRADIENT_ABSOLUTE, RUNE_GRADIENT_RELATIVE } RuneGradientMode;
// ABSOLUTE: coordinates in scene/local space (pixels)
// RELATIVE: coordinates in [0,1] normalized to the draw rect (like WPF RelativeToBoundingBox)
typedef enum { RUNE_GRADIENT_CLAMP, RUNE_GRADIENT_REPEAT, RUNE_GRADIENT_MIRROR } RuneGradientTileMode;
typedef enum { RUNE_CAP_BUTT, RUNE_CAP_ROUND, RUNE_CAP_SQUARE } RuneLineCap;
typedef enum { RUNE_JOIN_MITER, RUNE_JOIN_ROUND, RUNE_JOIN_BEVEL } RuneLineJoin;
typedef enum { RUNE_BLEND_NORMAL, RUNE_BLEND_MULTIPLY, RUNE_BLEND_SCREEN, RUNE_BLEND_OVERLAY, RUNE_BLEND_DST_IN, RUNE_BLEND_SRC_IN, RUNE_BLEND_SRC_ATOP } RuneBlendMode;
typedef enum { RUNE_IMAGE_FILL_MODE_STRETCH, RUNE_IMAGE_FILL_MODE_TILE, RUNE_IMAGE_FILL_MODE_TILE_X, RUNE_IMAGE_FILL_MODE_TILE_Y } ImageFillMode;
typedef enum { RUNE_IMAGE_INTERPOLATION_NEAREST, RUNE_IMAGE_INTERPOLATION_LINEAR, RUNE_IMAGE_INTERPOLATION_HIGH_QUALITY } ImageInterpolation;

Representative Record Layouts

cpp
struct CmdHeader          { uint8_t op; uint8_t sub_type; uint16_t size; };
struct CmdDrawPath        { CmdHeader header; Path path; Brush brush; };
struct CmdStrokePath      { CmdHeader header; Path path; Value width; float miter_limit; float dash_offset; Brush brush; uint8_t cap, join, dash_count; };
struct CmdStrokeTrimmedPath { CmdHeader header; Path path; Value width, trim_start, trim_end, trim_offset; float miter_limit; Brush brush; uint8_t cap, join; uint16_t reserved; };
struct CmdDrawDoubleRRect { CmdHeader header; Value x, y, w, h, radius, border_width; Brush fill_brush; Brush border_brush; };
struct CmdDrawBoxShadow   { CmdHeader header; Value x, y, w, h, radius, offset_x, offset_y, blur_radius, spread, c0, c1, c2, alpha; uint8_t color_space; };
struct CmdDrawBoxShadow4  { CmdHeader header; Value x, y, w, h, top_left, top_right, bottom_right, bottom_left, offset_x, offset_y, blur_radius, spread, c0, c1, c2, alpha; uint8_t color_space; };
struct CmdDrawShapedText  { CmdHeader header; ShapedText* shaped; uint32_t glyph_start, glyph_count; Value x, y; Brush brush; };
struct BrushLinearGradient  { CmdHeader header; ResourceSlot* slot; uint8_t color_space, gradient_mode, tile_mode, stop_count; Value start_x, start_y, end_x, end_y; /* stops[] */ };
struct BrushRadialGradient { CmdHeader header; ResourceSlot* slot; uint8_t color_space, gradient_mode, tile_mode, stop_count; Value cx, cy, rx, ry, focal_x, focal_y; /* stops[] */ };
struct BrushConicGradient  { CmdHeader header; ResourceSlot* slot; uint8_t color_space, gradient_mode, tile_mode, stop_count; Value cx, cy, start_angle, end_angle; /* stops[] */ };
struct DataPath           { DataHeader header; ResourceSlot* slot; uint64_t static_hash; float bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y; float total_length, flattening_tolerance; uint32_t bytecode_size, knot_count; uint8_t fill_rule, flags; uint16_t reserved0; uint32_t segment_count; /* bytecode[]; MeasuredPathSegment[]; PathMetricKnot[] */ };
struct CmdDrawChild       { CmdHeader header; uint16_t child_slot; };
struct CmdPushClipPath    { CmdHeader header; Path path; };

Examples

Shared Brush ResourceSlot Reuse

text
[CMD_DATA solid brush] slot=theme_slot → brush_id=offset
[DATA_PATH] slot=icon_slot fillRule=nonZero → path_id=offset
[CMD_DRAW_RECT brush=brush_id]
[CMD_DRAW_PATH path=path_id, brush=brush_id]

One slot-configured brush can be reused by multiple commands via its Brush offset, and the slot-owned D2D realization can persist across frames when ISlotResource::config_hash says the resolved configuration is unchanged.

Image Fill without CMD_DRAW_IMAGE

text
[CMD_DATA image brush] slot=image_slot → brush_id=offset
[CMD_DRAW_RECT x y w h brush=brush_id]

Image drawing is ordinary brush-plus-geometry recording. The Image is a snapshot resource reference, while the ResourceSlot owns the ID2D1ImageBrush realization and the D2D device cache shares the uploaded bitmap across slots.

Typed Path Recording

text
[DATA_PATH] slot=blob_slot fillRule=nonZero bytecode=...
[CMD_DRAW_PATH path=path_id, brush=brush_id]

The path geometry is snapshot-owned because Path points at DATA_PATH inside the snapshot data_buffer, while the slot-owned D2DPathResource can persist across frames when the resolved geometry hash stays stable.

Double Rounded Rect with Two Brush Slots

text
[CMD_DATA linear gradient] slot=fill_slot → fill_brush_id
[CMD_DATA solid brush] slot=border_slot → border_brush_id
[CMD_DRAW_DOUBLE_RRECT fill_brush=fill_brush_id, border_brush=border_brush_id]

The primitive resolves two slot-owned brush images and keeps fill and border disjoint.

Rounded Box Shadow

text
[CMD_DRAW_BOX_SHADOW x y w h radius offset blur spread color]
[CMD_DRAW_RRECT x y w h radius brush=surface_brush]

Box shadow is a direct solid-color effect command. It expands bounds by offset, spread, and blur, then the caster can be drawn normally on top.

Child Composition as a Draw Command

text
[CMD_SET_OFFSET]
[CMD_DATA solid brush] slot=panel_slot → brush_id
[DATA_PATH] slot=outline_slot → path_id
[CMD_DRAW_RRECT brush=brush_id]
[CMD_DRAW_CHILD child_slot=2]
[CMD_STROKE_PATH path=path_id, brush=brush_id]

The child is drawn in declaration order, contributes bounds like any other draw, and can change independently because commit can publish a new parent snapshot shell with an updated child_slots[2] entry without rewriting the parent buffer.

Performance

  • One contiguous command buffer enables linear replay and cheap skipping through header.size.
  • 8-byte alignment preserves natural alignment for pointer-bearing records.
  • The separate 1 MB command/data caps keep per-snapshot memory bounded while leaving Brush and Path offset space roomy.
  • Resolving Brush through cmd_buffer and Path through typed data_buffer keeps paint and geometry lookup uniform while removing large path payloads from the command-size limit.
  • ISlotResource::config_hash lets both brush and path slots skip rebuilds when the resolved configuration is unchanged.
  • Full trimmed stroke reuses the complete slot-cached path; only partial geometry is reconstructed.
  • Path removes tagged-pointer branching and shared-path special cases.
  • CMD_DRAW_CHILD lets dirty children publish new snapshots without forcing parent command-buffer rebuilds.
  • The unified image-based D2D pipeline keeps solid, gradient, image, text tint, and double-rrect on one replay path.

Alternatives Considered

ApproachRejected because
Inline fill payloads on every draw commandDuplicates paint data and prevents brush reuse
Separate op variants for static path and animated pathOne Path + DATA_PATH model already handles both without widening the op set
Dedicated CMD_DRAW_IMAGE opImage fill fits the same brush-plus-geometry model as other paint
Treat child nodes as snapshot-owned children[] metadataBreaks command uniformity and forces parent rebuilds when only child snapshots change
Native D2D brush objects for ordinary replayNative D2D brushes do not fit the unified ID2D1Image pipeline, cannot feed double-rrect's two-input shader path directly, and would split slot-owned realization logic across backends
Per-frame brush cache keyed by command-buffer identityResourceSlot already owns the persistent D2D realization, so an ephemeral replay cache duplicates state and loses cross-frame reuse
Separate side buffer for animated pathsAdds another lifetime and publication mechanism when snapshot data_buffer already carries immutable DATA_PATH records
Dash emulation for path trimCannot preserve cap, seam-wrap, offset, or SamplePath synchronization semantics
Store trim on PathMakes reusable immutable geometry stateful and prevents independent fill/full/trimmed uses
Use D2D path measurement as the canonical metricWould let visible trim drift from backend-neutral SamplePath
Unbounded command bufferBreaks predictable per-snapshot memory bounds and still cannot lift the 64 KiB per-command cap imposed by CmdHeader::size

Dependencies

  • RUNE-301 defines Value, used by property ops, geometry fields, gradient stops, and path coordinates.
  • RUNE-303 defines snapshot publication and traversal, including parent snapshot shells during commit and linear replay on the render thread.
  • RUNE-308 defines the ownership rules for images, nine-slices, shaped text, brush slots, path slots, snapshot-local anim arrays, and child-slot references.

Test Strategy

  1. Verify every record writes a valid CmdHeader with op, sub_type, and 8-byte-aligned size.
  2. Verify RuneCommandOp contains 27 ops, including CMD_STROKE_TRIMMED_PATH.
  3. Verify CmdDrawChild records uint16_t child_slot and resolves through the owning snapshot's child_slots[] array.
  4. Verify CMD_DRAW_CHILD participates in structural bounds through the referenced child snapshot's projected bounds and replays through immutable child_slots[].
  5. Verify draw, stroke, and text commands store Brush handles (cmd_buffer offsets), and BrushSolid, BrushLinearGradient, BrushRadialGradient, BrushConicGradient, and BrushImage records carry ResourceSlot* slot.
  6. Verify CMD_DATA differentiates the active DATA_BRUSH_* subtypes through sub_type, while paths resolve through typed DATA_PATH records in data_buffer.
  7. Verify CmdDrawPath, CmdStrokePath, and CmdPushClipPath carry Path handles that resolve to DATA_PATH records in the snapshot data_buffer.
  8. Verify push_with_extra() rejects any single aligned command larger than 0xFFFF, both buffers reject writes beyond the 1 MB (0x100000) cap, brush-recording APIs return Brush, rune_node_create_path() returns Path, and pointer-bearing records remain valid up to the overall buffer caps.
  9. Verify CMD_DRAW_DOUBLE_RRECT resolves fill_brush and border_brush offsets to slot pointers and preserves non-overlapping fill and border regions.
  10. Verify CMD_DRAW_BOX_SHADOW expands bounds by offset, spread, and blur, clips the shadow out of the caster interior, and applies solid color plus replay opacity.
  11. Verify image fill uses DATA_BRUSH_IMAGE plus ordinary geometry, and ResourceSlot realizations persist across frames when ISlotResource type tag is compatible, updating only dynamic Value parameters via SetValue().
  12. Verify DATA_PATH v2 fixed/header sizes, segment/knot topology, exact boundary ties, Close, zero-length paths, hostile counts, and aggregate capacity failure.
  13. Verify trimmed stroke empty/full/partial/wrapped behavior on open and closed paths, animated start/end/offset liveness, curve/follower agreement, and cap geometry.
  14. Verify Direct2D partial extraction preserves source curves while full trim reuses complete geometry.
  15. Verify trimmed-stroke bounds and relative brushes use complete style-expanded source bounds and remain stable as trim animates.
  16. Verify RuneSvg owns its retained root snapshot, CMD_DRAW_SVG participates in value/bounds analysis, and replay maps the viewBox into animated destination values.

Rune Project brings Rune Story authoring together with the Rune Engine rendering foundation.