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.
| Field | Value |
|---|---|
| Language | C# (.NET) |
| License | MIT (licence.md) |
| Repository | AvaloniaUI/Avalonia |
| Documentation | docs.avaloniaui.net |
| Category | multi-backend toolkit seam |
| Pinned revision | aee3f68551b0ac4417e32996a6627f34462edbc3 |
| Target range | desktop (Win32, X11, macOS), mobile (Android, iOS), browser (WASM), headless |
| Backends shipped at this revision | Skia (src/Skia/Avalonia.Skia) and headless (src/Headless/Avalonia.Headless) only — no Direct2D backend exists in the tree |
| Seam declaration | src/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:
/// <summary>
/// Defines the interface through which drawing occurs.
/// </summary>
[Unstable]
public interface IDrawingContextImpl : IDisposableIPlatformRenderInterface 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:
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:
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
}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:
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);
}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, andTryMatchCharacter(int codepoint, …, out IPlatformTypeface), i.e. font fallback per codepoint.ITextShaperImpl—ShapedBuffer 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:
_metrics = DrawAndCalculateMetrics(
null, // drawing context
new Point(), // drawing offset
true); // calculate black box metricsThat 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):
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.
| Scope | Mechanism | Declared where | Example |
|---|---|---|---|
| Whole platform, static | bool / enum properties on an interface | IPlatformRenderInterface | SupportsIndividualRoundRects, SupportsRegions, DefaultPixelFormat, DefaultAlphaFormat, IsSupportedBitmapPixelFormat(PixelFormat) |
| Graphics context, runtime | object? TryGetFeature(Type) | IOptionalFeatureProvider | IExternalObjectsHandleWrapRenderInterfaceContextFeature |
| One render target, per-frame | readonly struct of init booleans | RenderTargetProperties.cs | RetainsPreviousFrameContents, IsSuitableForDirectRendering, PreviousFrameIsRetained |
| One drawing context | optional interface + GetFeature(Type) | IDrawingContextImpl | IDrawingContextWithAcrylicLikeSupport, 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:
_backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>()
.SupportsIndividualRoundRects;
…
if (borderThickness.IsUniform &&
(cornerRadius.IsUniform || _backendSupportsIndividualCorners == true) &&
backgroundSizing == BackgroundSizing.CenterBorder)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:
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
if (_impl is IDrawingContextWithAcrylicLikeSupport idc)
idc.DrawRectangle(material, rect);
else
DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect);
}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:
abstract class RenderDataBrushAndPenNode : IRenderDataItemWithServerResources
{
public IBrush? ServerBrush { get; set; }
public IPen? ServerPen { get; set; }
public IPen? ClientPen { get; set; }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:
public IRef<IGlyphRunImpl>? GlyphRun { get; set; }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:
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:
/// <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);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
BoundsandHitTest, 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
falsetoSupportsIndividualRoundRectsandSupportsRegions, 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/ClientPenduplication 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
| Decision | Rationale | Trade-off |
|---|---|---|
Mark the platform seam [Unstable]/[PrivateApi] | Lets the interface grow (DrawRegion, PushTextOptions) without deprecation cycles | Third-party backends are not a supported scenario; only in-tree backends track the churn |
No optional methods on IDrawingContextImpl | A backend author reads one file and knows the whole obligation | Every backend implements every method, including the ones it will only ever no-op |
Declare static capabilities as properties on IPlatformRenderInterface | Callers 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 structs | A capability whose lifetime is a frame cannot be a static boolean | Four mechanisms to learn instead of one |
One class per drawing operation, with Bounds + HitTest on each | Hit testing and extent are derivable from the stream without a parallel structure | Allocation per command; pooling (PooledInlineList, ThreadSafeObjectPool) becomes mandatory rather than optional |
| Semantic ops (acrylic) as optional interfaces with framework fallbacks | The degradation exists once and is driven by the semantic value's own FallbackColor | Discovering that acrylic exists requires knowing to look for IDrawingContextWithAcrylicLikeSupport |
Ref-count payloads (IRef<T>) and transfer ownership at serialisation | Commands legitimately outlive the frame and change thread | Every payload capture is a refcount operation; disposal correctness is a runtime property |
| Text never enters the seam unshaped | The painter is not a party to measurement; DrawGlyphRun is trivially portable | A backend cannot apply its own shaping optimisations; the headless backend must still ship a real font |
Bearing on the proposal
- 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 havepushClip?) 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. - Give optional probes a declared type. Replace bare
__traits(compiles)at interpreter call sites with named capability traits or agetFeature-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. - Keep
scrollbarsemantic, 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 ofScrollbar's fourteen fields,trackGlyphandthumbGlyph, off every pixel backend's concern, and leaves the remaining twelve to one degradation written once, without giving up the semantics. - 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 exhaustivematch!over the sum's eight arms, but only where the query is written that way rather than as tests against the derivedkind. That is the guarantee F3's endorsement of reification leaves implicit. - A scene-side extent query is normal, and it must carry its scale. Avalonia computes a cached
Boundsby unioning per-node bounds, uses it for scene brushes, and passesRenderTargetSceneInfo(PixelSize, Scaling, LogicalSize)forward to the surface — F7's maintained-at-construction camp, on both axes. Friction §8's hand-rolled fold inskia-canvas-render.dis not a symptom of asking the wrong question; it is the right query, unimplemented, and therefore asked the expensive way. IRef-style refcounting is the confirmed answer to §7, and the handoff must be explicit.SerializeChanges'_itemsSentflag is a one-bit ownership transfer between threads — exactly what M7/T5's record-here-submit-there plan needs, and cheaper than interning.- Test-backend divergence is a feature. Headless answering
falsewhere Skia answerstruekeeps the fallback paths live.RecordingCanvasshould deliberately decline some optional capabilities rather than implementing everything, so the degradation paths are exercised by the reference backend. - 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
src/Avalonia.Base/Platform/IDrawingContextImpl.cs— the seam: 27 methods,Transform,GetFeature, plusIDrawingContextImplWithEffectsandIDrawingContextLayerImpl.src/Avalonia.Base/Platform/IPlatformRenderInterface.cs— the declared capability set (SupportsIndividualRoundRects,SupportsRegions,DefaultPixelFormat,DefaultAlphaFormat) and the resource factories, incl.CreateGlyphRun;IPlatformRenderInterfaceContextandMaxOffscreenRenderTargetPixelSize.src/Avalonia.Base/Platform/ITextShaperImpl.cs,IFontManagerImpl.cs,IGlyphRunImpl.cs— the three-interface text stack that never touches the painter.src/Avalonia.Base/Media/FormattedText.cs—DrawAndCalculateMetrics(null, …);Width/Height/Baseline/Extent.src/Avalonia.Base/Media/DrawingContext.cs— the public abstract painter, its*Coretemplate methods and thePushedStateRAII pair.src/Avalonia.Base/Media/PlatformDrawingContext.cs,IDrawingContextWithAcrylicLikeSupport.csandServerCompositionExperimentalAcrylicVisual.cs— a semantic op behind an optional interface, degrading tomaterial.FallbackColor.src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs,RenderDataRectangleNode.cs,RenderDataGlyphRunNode.cs,RenderDataLineNode.cs— the reified command stream as typed nodes.src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs— the recorder;GetServer,ClientPen,PlatformImpl.Clone().src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.csandServerCompositionRenderData.cs— the cross-thread handoff and the cached extent union.src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs— the one place a tag-plus-union encoding is used, and why.src/Avalonia.Base/Platform/IRenderTarget.csandRenderTargetProperties.cs—RenderTargetSceneInfo; per-target and per-frame capability structs.src/Avalonia.Base/Utilities/Ref.cs,IOptionalFeatureProvider.cs,Rendering/SceneGraph/CustomDrawOperation.cs,Media/ISceneBrush.cs.src/Avalonia.Controls/Utils/BorderRenderHelper.cs— the only in-tree consumer ofSupportsIndividualRoundRects.src/Skia/Avalonia.Skia/PlatformRenderInterface.cs,DrawingContextImpl.cs,src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs,HeadlessPlatformStubs.cs— the two shipped backends.src/Avalonia.Base/Layout/LayoutHelper.cs— DPI-scaled layout rounding, above the seam.
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>.