Skip to content

Avalonia IDrawingContextImpl — a primitive seam with capabilities declared at four different scopes

Category: multi-backend toolkit seam. Last reviewed: August 23, 2026. Pinned at aee3f685.

A shipping .NET toolkit whose renderer seam is close to sparkles:ui in size — one abstract platform painter, twenty-seven methods, two live backends — and the only surveyed subject that both reifies its command stream as typed nodes and declares its backend capabilities explicitly, which are the two halves of canvas-seam-friction.md §2 and §4.

FieldValue
LanguageC# (.NET)
LicenseMIT (licence.md)
RepositoryAvaloniaUI/Avalonia
Documentationdocs.avaloniaui.net
Categorymulti-backend toolkit seam
Pinned revisionaee3f68551b0ac4417e32996a6627f34462edbc3
Target rangedesktop (Win32, X11, macOS), mobile (Android, iOS), browser (WASM), headless
Backends shipped at this revisionSkia (src/Skia/Avalonia.Skia) and headless (src/Headless/Avalonia.Headless) only — no Direct2D backend exists in the tree
Seam declarationsrc/Avalonia.Base/Platform/IDrawingContextImpl.cs

NOTE

The Direct2D backend that historically sat beside Skia is not present at the pinned revision: src/Windows/ holds only Avalonia.Win32, Avalonia.Win32.Automation and Avalonia.Win32.Interoperability. The capability surface Direct2D motivated outlived it — CreateLayer's doc comment in IDrawingContextImpl.cs still justifies itself by saying "the Direct2D backend has to do a format conversion each time a standard render target bitmap is rendered". Statements below about "two backends" are statements about this revision.

Overview

What it solves

Avalonia separates the drawing API an application writes against (Avalonia.Media.DrawingContext, a public abstract class) from the drawing API a platform implements (Avalonia.Platform.IDrawingContextImpl). A control's Render(DrawingContext) never sees a backend. Between the two sits a recorder: RenderDataDrawingContext is a DrawingContext subclass that turns calls into a tree of typed IRenderDataItem nodes, which is serialised to a compositor thread and replayed there against the real IDrawingContextImpl. So the seam is crossed twice — once by the recorder that produces values, once by the replayer that consumes them.

Design philosophy

The seam is explicitly unstable and explicitly private. Both interfaces carry metadata attributes saying so:

csharp
/// <summary>
/// Defines the interface through which drawing occurs.
/// </summary>
[Unstable]
public interface IDrawingContextImpl : IDisposable

IDrawingContextImpl.cs

IPlatformRenderInterface carries [Unstable, PrivateApi] likewise. That is the load-bearing decision. Because the seam is not public API, Avalonia can add a method to it (DrawRegion, PushTextOptions) without a deprecation cycle, and therefore has no optional core methods at all: every one of IDrawingContextImpl's twenty-seven methods is mandatory. Optionality is expressed only outside the core interface, by the separate mechanisms tabulated under Q2 below.

How it works

IDrawingContextImpl declares one settable property (Matrix Transform) and twenty-seven methods. Eight of them draw:

csharp
void Clear(Color color);
void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect);
void DrawLine(IPen? pen, Point p1, Point p2);
void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry);
void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default);
void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region);
void DrawEllipse(IBrush? brush, IPen? pen, Rect rect);
void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun);

The remaining nineteen are eight Push/Pop pairs (Clip ×3 overloads, Layer, Opacity, OpacityMask, GeometryClip, RenderOptions, TextOptions), CreateLayer, and GetFeature. There is no Measure, no FillRect, no text-string entry point — DrawGlyphRun takes an already-shaped IGlyphRunImpl.

The command stream is reified as one class per operation, not as a tagged record. The contract every node satisfies is three members wide:

csharp
interface IRenderDataItem
{
    void Invoke(ref RenderDataNodeRenderContext context); // render to a drawing context
    Rect? Bounds { get; }                                 // visible content, global coords
    bool HitTest(Point p);                                // this node's geometry only
}

RenderDataNodes.cs

Nine node classes implement it (RenderDataRectangleNode, RenderDataGlyphRunNode, RenderDataLineNode, RenderDataEllipseNode, RenderDataGeometryNode, RenderDataBitmapNode, RenderDataCustomNode, plus the RenderDataPushNode family for the push/pop pairs). Each carries only the fields its operation uses, and each supplies its own Bounds and HitTest:

csharp
class RenderDataRectangleNode : RenderDataBrushAndPenNode
{
    public RoundedRect Rect { get; set; }
    public BoxShadows BoxShadows { get; set; }

    public override void Invoke(ref RenderDataNodeRenderContext context) =>
        context.Context.DrawRectangle(ServerBrush, ServerPen, Rect, BoxShadows);

    public override Rect? Bounds => BoxShadows.TransformBounds(Rect.Rect).Inflate((ServerPen?.Thickness ?? 0) / 2);
}

RenderDataRectangleNode.cs

RenderDataPushNode owns a PooledInlineList<IRenderDataItem> Children and brackets them, so the stream is a tree, not a flat list with balance obligations — a Push with no children is skipped entirely (if (Children.Count == 0) return;).

Q1 — measurement units, and who answers

Not on the painter, and not even on one interface. Text reaches IDrawingContextImpl only as IGlyphRunImpl, an "immutable platform representation" that already knows its own Bounds, BaselineOrigin and FontRenderingEmSize (IGlyphRunImpl.cs). Producing it is a three-interface job that the drawing context has no part in:

  • IFontManagerImpl — family enumeration, TryCreateGlyphTypeface, and TryMatchCharacter(int codepoint, …, out IPlatformTypeface), i.e. font fallback per codepoint.
  • ITextShaperImplShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options).
  • IPlatformRenderInterface.CreateGlyphRun(GlyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo>, Point baselineOrigin) (IPlatformRenderInterface.cs) — the shaped result becomes a platform glyph run.

The unit is a double in device-independent pixels throughout; there is no per-backend length type as in Slint. The measurement API a control uses, FormattedText, exposes Width, Height, Baseline, Extent, OverhangLeading/OverhangTrailing and WidthIncludingTrailingWhitespace — and computes them by running the draw path with a null painter:

csharp
_metrics = DrawAndCalculateMetrics(
    null,           // drawing context
    new Point(),    // drawing offset
    true);          // calculate black box metrics

FormattedText.cs

That is the sharpest statement of F1 in the survey: measuring is drawing with the backend removed, which is only possible because the backend contributes nothing to the answer.

The headless backend proves the same point from the other side. It stubs every geometry, bitmap and drawing call to a no-op, but it cannot stub the font — it ships a real TrueType file, BareMinimum.ttf, and loads it as the default typeface (HeadlessPlatformStubs.cs):

csharp
var defaultFontUri = new Uri("resm:Avalonia.Headless.BareMinimum.ttf?assembly=Avalonia.Headless");

A backend may be free of pixels; it is not free of metrics. Friction §1 has our SkiaCanvas.measure discarding Skia and returning cellsOf(text); Avalonia's equivalent question never reaches a canvas.

Q2 — is the contract stated in one place?

No — it is stated in four places, at four different scopes, and that is deliberate. This is the subject's most transferable finding and it complicates F5.

ScopeMechanismDeclared whereExample
Whole platform, staticbool / enum properties on an interfaceIPlatformRenderInterfaceSupportsIndividualRoundRects, SupportsRegions, DefaultPixelFormat, DefaultAlphaFormat, IsSupportedBitmapPixelFormat(PixelFormat)
Graphics context, runtimeobject? TryGetFeature(Type)IOptionalFeatureProviderIExternalObjectsHandleWrapRenderInterfaceContextFeature
One render target, per-framereadonly struct of init booleansRenderTargetProperties.csRetainsPreviousFrameContents, IsSuitableForDirectRendering, PreviousFrameIsRetained
One drawing contextoptional interface + GetFeature(Type)IDrawingContextImplIDrawingContextWithAcrylicLikeSupport, IDrawingContextImplWithEffects, ISkiaSharpApiLeaseFeature

The static tier is a genuine declared capability set in Qt's sense, and it is consumed by name, not probed. BorderRenderHelper asks once and caches the answer, then chooses between a fast path and building retained geometry:

csharp
_backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>()
    .SupportsIndividualRoundRects;

if (borderThickness.IsUniform &&
    (cornerRadius.IsUniform || _backendSupportsIndividualCorners == true) &&
    backgroundSizing == BackgroundSizing.CenterBorder)

BorderRenderHelper.cs

The interface's own doc comment states the contract for the caller, not the implementor: "Some platform renderers can't directly handle rounded corners on rectangles. In this case, code that requires rounded corners must generate and retain a geometry instead." (IPlatformRenderInterface.cs) The two shipped backends answer differently — Skia => true (PlatformRenderInterface.cs), headless => false (HeadlessPlatformRenderInterface.cs) — so the fallback path is exercised by the test backend on every headless run. That is the property RecordingCanvas gives us and the reason it pays for itself.

The escape-hatch tier is the closest analogue of our __traits(compiles) probing, and it is deliberately not free-form: GetFeature is keyed by a declared feature interface, so the probeable set is enumerable by grepping for implementations. Skia's whole answer is one comparison against typeof(ISkiaSharpApiLeaseFeature), returning null otherwise (DrawingContextImpl.cs).

IMPORTANT

The lesson for friction §2 is not "declare your capabilities" but "a capability has a scope". pushClip is a property of a canvas type; "does this surface retain last frame" is a property of one render target on one frame. sparkles:ui probes four optional primitives — rule, scrollbar, pushClip and popClip — with __traits(compiles) at the interpreter call site: one undifferentiated bucket, with no vocabulary for the difference.

Q3 — semantic or primitive operations?

Primitive in the core, semantic through optional interfaces. There is no DrawScrollBar. Avalonia's ScrollBar is a templated control that renders as borders and rectangles like anything else, and the seam never learns it existed.

But the seam is not purely geometric either. DrawRectangle takes a RoundedRect and a BoxShadows — both semantic in exactly Slint's sense: the backend is told a shadow was intended rather than handed the blurred geometry. And the one genuinely semantic material, acrylic, enters as an optional interface with a framework-level fallback:

csharp
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
    if (_impl is IDrawingContextWithAcrylicLikeSupport idc)
        idc.DrawRectangle(material, rect);
    else
        DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect);
}

PlatformDrawingContext.cs

Three things are worth taking from those six lines. The degradation lives once, in the framework — Qt's camp in F4, not Slint's. The fallback is carried by the semantic value itself (material.FallbackColor), so the framework need not invent an approximation. And the semantic operation is not in the core interface at all: a new backend implements IDrawingContextImpl and gets acrylic-as-solid-colour free.

That is a direct answer to friction §3. sparkles:ui has the outline of the same arrangement: scrollbar sits outside the five-method concept, and a canvas that declines it gets a stated degradation — paintScrollbarCells, one glyph per cell — from the interpreter. What differs is where the fallback is written and what accepting the op costs. The degradation is the toolkit's reconstruction of what a scrollbar looks like in cells, not something the value carries, and a canvas that does take the primitive takes all fourteen Scrollbar fields with it, trackGlyph and thumbGlyph included, plus the obligation to know what a scrollbar is. The Avalonia shape is: keep the semantic op, put it behind a declared optional interface, give the semantic payload its own fallback, and let the interpreter apply that fallback once.

The corresponding extension point for applications is ICustomDrawOperation, which requires the caller to supply exactly the things IRenderDataItem requires — Rect Bounds, bool HitTest(Point), void Render(ImmediateDrawingContext) (CustomDrawOperation.cs). A custom op is a node like any other.

Q4 — command shape

A polymorphic class per operation, not a tag plus fields. Nine node types share a three-member interface; RenderDataRectangleNode has exactly Rect and BoxShadows beyond its brush/pen base, RenderDataLineNode has exactly P1 and P2, RenderDataGlyphRunNode has exactly a glyph-run reference. Nothing in the tree corresponds to one record wide enough for every verb, and there is no enum over the drawing verbs at all: a node's class is its tag.

This is F3 from a third independent direction: egui reifies as a Rust enum Shape, sparkles:ui as a closed SumType over eight per-kind payloads, Avalonia as a C# class hierarchy. One idea, three encodings — and the trade between them is precisely what F3 holds open. Avalonia's variant pays an allocation per command and buys exact width, so a push node costs what a push node needs; DrawOp pays the widest payload on every operation and buys comparable value semantics, which is what makes a recorded stream diffable pairwise.

What Avalonia buys with virtual dispatch is a property the sum does not give for free: every node answers Bounds and HitTest itself, so adding an operation cannot forget to teach the extent and hit-test walkers about it. In D that property is recoverable — an exhaustive match! over DrawOp's eight arms is compiler-checked the same way — but only where the query is written as one, rather than as a chain of tests against the derived kind.

One place Avalonia does use tag-plus-union is instructive about when that encoding is correct: the compositor's deferred state stack stores pending push/pop commands as a PendingCommandType enum plus two [StructLayout(LayoutKind.Explicit)] unions of overlapping fields (DrawingContextProxy.PendingCommands.cs) — an internal, short-lived buffer whose job is to be discarded without ever being replayed, the one case where dead fields cost nothing because nobody reads the wrong one.

Q5 — sub-unit placement

Does not arise. Coordinates are double device-independent pixels throughout; Rect, Point, RoundedRect and Matrix are all continuous, and device pixels appear only at surface boundaries as PixelSize. The framework's answer to pixel snapping is a layout pass, not a drawing vocabulary: LayoutHelper.RoundLayoutValue/RoundLayoutSizeUp/RoundLayoutThickness take an explicit dpiScale and are applied when a parent has layout rounding enabled (LayoutHelper.cs).

This is the third subject in a row to dissolve friction §5 by having continuous coordinates, and it adds one detail the others do not: the snapping is parameterised by the scale factor and performed above the seam, so the painter is never asked where a hairline goes. RuleEdge has no counterpart because the question has already been answered in DIPs by the time drawing starts.

Q6 — resolved appearance, semantic role, or both?

Both, but along a different axis than ours — and the duplication is real. Every brush/pen node carries two references to the same styling:

csharp
abstract class RenderDataBrushAndPenNode : IRenderDataItemWithServerResources
{
    public IBrush? ServerBrush { get; set; }
    public IPen? ServerPen { get; set; }
    public IPen? ClientPen { get; set; }

RenderDataNodes.cs

ServerBrush/ServerPen come from brush.GetServer(_compositor) — the render-thread resolved resource, used by Invoke and by Bounds. ClientPen is the original UI-thread object, used by HitTest (RenderDataDrawingContext.cs, RenderDataLineNode.cs). So the duplication is not semantic role versus resolved appearance; it is the same appearance held at two thread affinities, because the two consumers of a node run on different threads.

Friction §6 says sparkles:ui "hedges rather than deciding": six of DrawOp's eight payloads store a Slot beside the resolved colours the primitive actually paints from, and reconstructing Visual on demand instead of storing it makes the hedge cheaper without making it a decision. Avalonia hedges too, and pays a comparable per-node cost — but its second field buys thread-safe hit testing, a capability, rather than serving a second backend. The question friction §6 should ask is therefore sharper than "which one?": what does the second field buy, and is that thing a capability or a consumer? If the HTML interpreter is the only consumer of slot, it is a consumer and the cost is misplaced; if slot is what makes a display list re-themeable without re-layout, it is a capability and it is earning its space.

Q7 — payload ownership

Reference-counted, with an explicit handoff. Glyph runs are the model case:

csharp
public IRef<IGlyphRunImpl>? GlyphRun { get; set; }

RenderDataGlyphRunNode.cs

IRef<out T> is "a ref-counted wrapper for a disposable object" with Clone(), IsAlive and RefCount (Ref.cs), and the recorder increments on capture: GlyphRun = glyphRun.PlatformImpl.Clone().

Nothing is borrowed for the duration of a call. Commands are explicitly built to outlive the frame and cross a thread: CompositionRenderData.SerializeChanges writes the node objects into a BatchStreamWriter and marks _itemsSent = true, after which the client side stops disposing them (CompositionRenderData.cs) — an ownership transfer, spelled out in one flag.

This is F8 confirmed and extended, and it is the axis on which sparkles:ui stands apart: TextRun.text is a const(char)[] borrowed from a frame arena, and the rule stated on the type — an operation is valid while the buffer that built it is alive and unreset — is a frame-scoped borrow, not a share. Friction §7 notes that "a GPU backend that wants to record on one thread and submit on another meets it immediately"; Avalonia's entire architecture is that arrangement, and it costs a refcounted handle type plus a transfer flag, neither an interning table nor a copy per run. UI-O4 stays open on exactly that retain boundary.

Q8 — can a backend ask the scene its extent?

Yes, in both directions — F7's scene-side camp at its most complete.

Scene → extent: every node declares Rect? Bounds, and the recorded list folds them:

csharp
private LtrbRect? CalculateRenderBounds()
{
    LtrbRect? totalBounds = null;
    foreach (var item in _items)
        totalBounds = LtrbRect.FullUnion(totalBounds, item.Bounds);

    return ApplyRenderBoundsRounding(totalBounds);
}

ServerCompositionRenderData.cs

The result is cached behind a _boundsValid flag and rounded outward (Math.Floor/Math.Ceiling). This is the same union skia-canvas-render.d folds by hand in friction §8 — except that each node contributes its own bounds as it is built, so the union is a cached, first-class API rather than a caller walking op.rect across the whole stream, and it is what makes ISceneBrushContent.Rect (ISceneBrush.cs) possible: recorded content used as a brush must know its own extent.

Scene → surface: the extent also flows forward. IRenderTarget.CreateDrawingContext takes the scene's size as a parameter:

csharp
/// <param name="sceneInfo">Information about the scene that's about to be rendered into this render target.
/// This is expected to be reported to the underlying platform and affect the framebuffer size, however
/// the implementation may choose to ignore that information.
/// </param>
IDrawingContextImpl CreateDrawingContext(RenderTargetSceneInfo sceneInfo, out RenderTargetDrawingContextProperties properties);

public record struct RenderTargetSceneInfo(PixelSize Size, double Scaling, Size LogicalSize);

IRenderTarget.cs

F7 separates surface, layout and ink extent, and separates maintained-at-construction from derived-by-scan. Avalonia lands on the maintained side of both: bounds accumulate as the stream is recorded, and the scene's size then travels forward to the surface, which may decline it and say so through RenderTargetDrawingContextProperties on the same call. That the parameter carries PixelSize, Scaling and LogicalSize together is the detail worth copying — extent without its scale is not actionable at a seam that spans units.

Strengths

  • Optionality has a type. GetFeature(Type) returning a declared feature interface makes the probeable surface enumerable, unlike an untyped __traits(compiles) probe at a call site.
  • Capabilities are scoped. Platform-static, context-runtime, per-target and per-context tiers each answer a question with the right lifetime.
  • One field per operation. Nine node classes with no dead fields, each supplying its own Bounds and HitTest, so extending the stream cannot leave the extent or hit-test walker behind.
  • Ownership is explicit end-to-end. IRef<T> plus a serialisation handoff makes "this command outlives the frame and changes thread" a stated property.
  • Semantic operations arrive without widening the core, and the test backend exercises the fallback paths — headless answers false to SupportsIndividualRoundRects and SupportsRegions, so the geometry fallback is not dead code.

Weaknesses

  • The seam is unstable by declaration. [Unstable]/[PrivateApi] is what makes twenty-seven mandatory methods tolerable; a toolkit that promises a stable backend API cannot buy that freedom.
  • Twenty-seven mandatory methods is a real cost. The headless backend is ~180 lines of empty bodies, most of which exist only to satisfy the interface.
  • Four capability mechanisms is three more than a reader wants. Finding out what a backend must support means reading an interface, a struct, a feature registry and a set of optional interfaces.
  • The ServerBrush/ClientPen duplication is per-node and unavoidable, and the reason (thread affinity of the hit tester) is documented nowhere near the fields.

Key design decisions and trade-offs

DecisionRationaleTrade-off
Mark the platform seam [Unstable]/[PrivateApi]Lets the interface grow (DrawRegion, PushTextOptions) without deprecation cyclesThird-party backends are not a supported scenario; only in-tree backends track the churn
No optional methods on IDrawingContextImplA backend author reads one file and knows the whole obligationEvery backend implements every method, including the ones it will only ever no-op
Declare static capabilities as properties on IPlatformRenderInterfaceCallers can choose an algorithm once and cache it (BorderRenderHelper)Capability granularity is fixed at compile time; a backend cannot vary it per surface
Route runtime/per-frame capabilities through TryGetFeature and property structsA capability whose lifetime is a frame cannot be a static booleanFour mechanisms to learn instead of one
One class per drawing operation, with Bounds + HitTest on eachHit testing and extent are derivable from the stream without a parallel structureAllocation per command; pooling (PooledInlineList, ThreadSafeObjectPool) becomes mandatory rather than optional
Semantic ops (acrylic) as optional interfaces with framework fallbacksThe degradation exists once and is driven by the semantic value's own FallbackColorDiscovering that acrylic exists requires knowing to look for IDrawingContextWithAcrylicLikeSupport
Ref-count payloads (IRef<T>) and transfer ownership at serialisationCommands legitimately outlive the frame and change threadEvery payload capture is a refcount operation; disposal correctness is a runtime property
Text never enters the seam unshapedThe painter is not a party to measurement; DrawGlyphRun is trivially portableA backend cannot apply its own shaping optimisations; the headless backend must still ship a real font

Bearing on the proposal

  1. A capability has a scope; say which. Friction §2 asks for the contract to be stated. Avalonia shows that one statement is not enough: split sparkles:ui's single optional bucket into type-level (does this canvas have pushClip?) and instance/frame-level (does this surface retain the previous frame? what is its scale?). This complicates F5, whose floor / defaulted / refusable ladder sorts primitives by severity — the missing axis is lifetime, not just severity.
  2. Give optional probes a declared type. Replace bare __traits(compiles) at interpreter call sites with named capability traits or a getFeature-style lookup keyed by a declared type, so the probeable surface is enumerable from one place. This is the cheap half of §2 and does not require abandoning structural typing.
  3. Keep scrollbar semantic, but let the op carry its own fallback instead of the interpreter reconstructing one. Avalonia's acrylic path — optional interface, framework-level degrade, fallback carried by the value — is the shape friction §3 is groping for. It takes the cell-only half of Scrollbar's fourteen fields, trackGlyph and thumbGlyph, off every pixel backend's concern, and leaves the remaining twelve to one degradation written once, without giving up the semantics.
  4. Write extent and hit testing as exhaustive matches over DrawOp. Avalonia gets "you cannot add an operation and forget its bounds" from virtual dispatch; D gets the same guarantee from an exhaustive match! over the sum's eight arms, but only where the query is written that way rather than as tests against the derived kind. That is the guarantee F3's endorsement of reification leaves implicit.
  5. A scene-side extent query is normal, and it must carry its scale. Avalonia computes a cached Bounds by unioning per-node bounds, uses it for scene brushes, and passes RenderTargetSceneInfo(PixelSize, Scaling, LogicalSize) forward to the surface — F7's maintained-at-construction camp, on both axes. Friction §8's hand-rolled fold in skia-canvas-render.d is not a symptom of asking the wrong question; it is the right query, unimplemented, and therefore asked the expensive way.
  6. IRef-style refcounting is the confirmed answer to §7, and the handoff must be explicit. SerializeChanges' _itemsSent flag is a one-bit ownership transfer between threads — exactly what M7/T5's record-here-submit-there plan needs, and cheaper than interning.
  7. Test-backend divergence is a feature. Headless answering false where Skia answers true keeps the fallback paths live. RecordingCanvas should deliberately decline some optional capabilities rather than implementing everything, so the degradation paths are exercised by the reference backend.
  8. Do not copy the [Unstable] bargain unless the same freedom is wanted. Twenty-seven mandatory methods is affordable only because Avalonia can change the interface at will; sparkles:ui's optional-primitive bargain is the right trade for a seam outside code may implement.

Sources

Revision pinned with git rev-parse HEAD against a local clone of AvaloniaUI/Avalonia; every cited path verified present at that SHA with git cat-file -e <sha>:<path>.