Java2D — the seam is a state vector, and the fallback is a superclass call
Category: framework-side emulation. Last reviewed: August 23, 2026. Pinned at d3e5304c.
The other way to build Qt's answer. Qt declares a PaintEngineFeature bitmask and lets QPainter emulate what is missing; Java2D declares nothing, resolves every draw against a three-key registry of rendering loops, and when the lookup misses it silently manufactures a software loop. A device advertises acceleration by overriding a method and delegating the rest to super, which makes the software floor structurally unforgettable — and completely invisible to the caller.
| Field | Value |
|---|---|
| Language | Java (public + framework layers), C/Objective-C (native loops) |
| License | GPLv2 with Classpath exception |
| Repository | openjdk/jdk, module java.desktop |
| Documentation | java.awt.Graphics2D class javadoc |
| Category | framework-side emulation |
| Pinned revision | d3e5304c0f70aa03a52f5449cb38645a184b23dc |
| Public seam | java.awt.Graphics2D — abstract class; every drawing verb is primitive (shapes, glyph runs, images) |
| Backend seam | sun.java2d.SurfaceData + the sun.java2d.pipe role interfaces |
| Backends shipped | software (BufImgSurfaceData, with the marlin rasterizer), X11, XRender (xr), OpenGL, Direct3D (d3d), Metal |
| Target range | 72 dpi screens, HiDPI, printers, metafiles, offscreen BufferedImage |
Overview
What it solves
One public drawing API — shapes, text, images, in floating-point user space — must land on a raster whose pixel format, compositing capability and text rasterizer are all unknown at the call site, and must produce the same picture whether the destination is a GPU-backed window, an in-heap int[] of ARGB pixels, or a printer. Graphics2D states the coordinate contract directly:
All coordinates passed to a
Graphics2Dobject are specified in a device-independent coordinate system called User Space, which is used by applications. TheGraphics2Dobject contains anAffineTransformobject as part of its rendering state that defines how to convert coordinates from user space to device-dependent coordinates in Device Space.
Design philosophy
The javadoc for the same class describes rendering as four abstract phases and then hands the implementation a broad licence to collapse them:
The renderer can optimize many of these steps, either by caching the results for future calls, by collapsing multiple virtual steps into a single operation, or by recognizing various attributes as common simple cases that can be eliminated by modifying other parts of the operation.
That licence is the whole architecture. Java2D never executes the four phases literally; it classifies the current rendering state into a small vector of ordinals, compiles that vector into a set of pipe objects, and caches the result. Nothing about the classification is visible above the seam, and nothing about the fallback is visible below it.
The second philosophical commitment is that the caller may request but never require. RenderingHints is explicit:
Note that since these keys and values are hints, there is no requirement that a given implementation supports all possible choices indicated below or that it can respond to requests to modify its choice of algorithm. … The full set of supported keys and hints may also vary by destination since runtimes may use different underlying modules to render to the screen, or to
BufferedImageobjects, or while printing. … Implementations are free to ignore the hints completely, but should try to use an implementation algorithm that is as close as possible to the request.
How it works
The state vector
SunGraphics2D is the single concrete Graphics2D for every destination. It does not hold a backend; it holds five ordinals plus five mutable pipe references:
public SurfaceData surfaceData;
public PixelDrawPipe drawpipe;
public PixelFillPipe fillpipe;
public DrawImagePipe imagepipe;
public ShapeDrawPipe shapepipe;
public TextPipe textpipe;
public MaskFill alphafill;
public RenderLoops loops;
public int paintState;
public int compositeState;
public int strokeState;
public int transformState;
public int clipState;Each ordinal is a complexity ladder, not an enumeration of kinds: PAINT_OPAQUECOLOR = 0 through PAINT_CUSTOM = 6; TRANSFORM_ISIDENT = 0 through TRANSFORM_GENERIC = 4; CLIP_DEVICE = 0, CLIP_RECTANGULAR = 1, CLIP_SHAPE = 2 (SunGraphics2D.java). Ordering matters: every capability test in the tree is written as <=, so a device that handles "anything up to an alpha colour" writes one comparison rather than a set membership test.
Pipe validation
When any attribute changes, SunGraphics2D installs ValidatePipe — a trampoline that revalidates on the next draw and then re-dispatches:
public void drawLine(SunGraphics2D sg,
int x1, int y1, int x2, int y2) {
if (validate(sg)) {
sg.drawpipe.drawLine(sg, x1, y1, x2, y2);
}
}Validation itself is SurfaceData.validatePipe(SunGraphics2D) — a 175-line decision tree over the state vector that assigns drawpipe, fillpipe, shapepipe, textpipe, imagepipe and alphafill from a fixed set of pipe singletons built once in a static initialiser (37 static final fields). There is a matching null-object, NullPipe, "useful for installing as the pipeline when the clip is determined to be empty or when the composite operation is determined to have no effect".
The seam is therefore per-state-change, not per-command. A draw call carries geometry only; appearance was resolved into the choice of pipe.
Loop lookup: a three-key registry with supertype fallback
Below the pipes are rendering loops — GraphicsPrimitive subclasses named by operation (Blit, FillRect, DrawLine, DrawGlyphListAA, MaskFill, …), each registered against a triple: a source SurfaceType, a CompositeType, and a destination SurfaceType. The triple is packed into one 32-bit key, eight bits per component:
public static final synchronized int makeUniqueID(int primTypeID,
SurfaceType src,
CompositeType cmp,
SurfaceType dst)
{
return (primTypeID << 24) |
(dst.getUniqueID() << 16) |
(cmp.getUniqueID() << 8) |
(src.getUniqueID());
}Both key types are chains, not flat enums. SurfaceType documents the contract:
Note that you cannot construct a brand new root for a chain since the constructor is private. Every chain of types must at some point derive from the
Anynode provided here using thederiveSubType()method. The presence of this commonAnynode on every chain ensures that all chains end with theDESC_ANYdescriptor so that a suitable GeneralGraphicsPrimitiveobject can be obtained for the indicated surface if all of the more specific searches fail.
GraphicsPrimitiveMgr.locatePrim is the triple loop over those chains — a lexicographic walk from most specific to most general:
for (dst = dsttype; dst != null; dst = dst.getSuperType()) {
for (src = srctype; src != null; src = src.getSuperType()) {
for (cmp = comptype; cmp != null; cmp = cmp.getSuperType()) {
spec.uniqueID =
GraphicsPrimitive.makeUniqueID(primTypeID, src, cmp, dst);
prim = locate(spec);
if (prim != null) {
return prim;
}
}
}
}
return null;If even that misses, locate asks for the registered general loop and lets it build one on the spot (GraphicsPrimitiveMgr.java):
if (prim == null) {
prim = GeneralPrimitives.locate(primTypeID);
if (prim != null) {
prim = prim.makePrimitive(srctype, comptype, dsttype);
}
}For Blit the last resort is AnyBlit, which pulls a Raster out of both surfaces and runs CompositeContext.compose span by span in Java (Blit.java) — correct for every combination, and the slowest path in the system. Nothing above it is told.
The pipe interfaces
The contract a pipe implements is split into role interfaces, each a handful of methods, in sun.java2d.pipe:
| Interface | Methods | Vocabulary |
|---|---|---|
PixelDrawPipe | 7 | drawLine, drawRect, drawRoundRect, drawOval, drawArc, drawPolyline, drawPolygon — integer device coords |
PixelFillPipe | 5 | the fill* mirror |
ShapeDrawPipe | 2 | draw(Shape) / fill(Shape) |
ParallelogramPipe | 2 | fillParallelogram / drawParallelogram, eight double coords |
TextPipe | 3 | drawString, drawGlyphVector, drawChars |
DrawImagePipe | 6 | copyImage, scaleImage, transformImage, … |
CompositePipe | 5 | the AA tile protocol: startSequence, needTile, renderPathTile, skipTile, endSequence |
LoopBasedPipe | 0 | a marker — "Pipes that need RenderLoops" |
LoopPipe implements five of them at once and forwards straight to the located loop. Everything else in the tree is an adapter between two of these interfaces: PixelToShapeConverter turns integer pixel calls into Shape calls, and PixelToParallelogramConverter recognises the cases that a ParallelogramPipe can take directly.
Q1 — measurement units, and who answers
Java2D is the third distinct answer in this survey, and the most interesting one: measurement is off the graphics context, but parameterised by it.
Graphics2D has no measure. It has getFontMetrics() and getFontRenderContext(). FontRenderContext is an immutable value object holding exactly three things — an AffineTransform, a text-antialiasing hint value, and a fractional-metrics hint value — and its javadoc states the dependence outright:
The
FontRenderContextclass is a container for the information needed to correctly measure text. The measurement of text can vary because of rules that map outlines to pixels, and rendering hints provided by an application. … A character that is rendered at 12pt on a 600dpi device might have a different size than the same character rendered at 12pt on a 72dpi device because of such factors as rounding to pixel boundaries and hints that the font designer may have specified. … AFontRenderContextwhich is directly constructed will most likely not represent any actual graphics device, and may lead to unexpected or incorrect results.
So the unit is device pixels, the answer depends on the device, and the dependence is reified as a value you can pass around rather than being implicit in a live painter. FontDesignMetrics memoizes on exactly that pair — MetricsKey(Font font, FontRenderContext frc) with hash = font.hashCode() + frc.hashCode(). A backend does not answer measurement; it contributes an FRC, and the shared font layer answers.
Two further details bear on friction §1:
- The legacy rung is integer.
FontMetrics.stringWidthreturnsint; the float-precision answer is a separate method,getStringBounds(String, Graphics), returningRectangle2Dand taking theGraphicspurely to extract its FRC via the privatemyFRC(context)helper. Java2D kept a coarse-unit measurement API alive beside a fine-grained one for thirty years, and the coarse one is the one everybody calls. - Per-character measurement is documented as wrong. The class javadoc: "Note that the advance of a
Stringis not necessarily the sum of the advances of its characters measured in isolation because the width of a character can vary depending on its context." That is a direct statement that a per-grapheme width sum does not generalise as a measurement primitive — and a per-grapheme width sum is precisely whatsparkles:ui'sSize measure(const(char)[])returns,Size(cellsOf(text), 1)overcellsOf, the toolkit's one width authority.
Q2 — is the contract stated, or discovered?
Stated, but in three separate registers, none of which is a capability query.
Role interfaces. A pipe declares what it can be asked by which of the eight
sun.java2d.pipeinterfaces it implements. This is Java's version of the__traits(compiles, …)probesparkles:uiapplies to its four optional primitives —rule,scrollbar,pushClip,popClip— made explicit at the type level:LoopPipe implements PixelDrawPipe, PixelFillPipe, ParallelogramPipe, ShapeDrawPipe, LoopBasedPipe, and the marker interfaceLoopBasedPipecarries no methods at all —SurfaceData.validatePipeends withif (sg2d.textpipe instanceof LoopBasedPipe || …) sg2d.loops = getRenderLoops(sg2d);.Registration. A device advertises an accelerated operation by registering a
GraphicsPrimitiveunder a(src, comp, dst)triple. There is no list of what a device supports; there is a sorted array of what it registered, andArrays.binarySearchover it.Override-and-delegate. The real capability declaration is a method override.
MTLSurfaceDataoverridesvalidatePipeand, for every state it cannot accelerate, callssuper.validatePipe(sg2d):java} else { // do this to initialize textpipe correctly; we will attempt // to override the non-text pipes below super.validatePipe(sg2d); textpipe = sg2d.textpipe; validated = true; }And the same file states the fallback contract for masked fills in a comment:
In all other cases, we return null, in which case the validation code will choose a more general software-based loop.
This is a genuinely different answer from Qt's, and it is the one that matters for friction §2. Qt states the contract as data (PaintEngineFeature) and the framework consults it. Java2D states it as inheritance: the software answer is the base-class implementation, and a backend is a partial override. The floor cannot be forgotten because forgetting it means not overriding, and not overriding means you get it.
The price is that nobody can enumerate what a device accelerates, including the device. SunGraphics2D.getRenderingHint(Key) returns the value that was set, never what the destination honoured; the accessor is a switch over the stored ordinals (SunGraphics2D.java). There is no hasFeature, no NODEGRADE, no way for a golden test to demand the fast path and fail otherwise.
Q3 — semantic operations, or primitives?
Pure primitives, and the layer above is a different library entirely.Graphics2D knows shapes, glyph runs and images. It has no notion of a border, a shadow, a focus ring or a scrollbar; Swing paints those, in Java, out of fillRect/draw(Shape) calls, above the seam.
Two consequences worth recording against friction §3.
First, it is possible. A toolkit of Swing's breadth was built on a handful of primitive verbs, so "the backend needs to know a scrollbar was intended" is not a law. It is a claim that only holds when a backend's fidelity floor is so low that primitives lose the intent — which is exactly the terminal case, and not a case Java2D has.
Second, Java2D pays for the primitivism precisely where fidelity is scarce. The one place semantics survive into the seam is text, and there the seam is three-rung rather than one: a caller may hand down a String (the Font performs "whatever basic layout and shaping algorithms the font implements"), an AttributedCharacterIterator (converted to a TextLayout that does bidi across fonts), or a GlyphVector that "already contains the appropriate font-specific glyph codes with explicit coordinates for the position of each glyph" (Graphics2D.java). Where a target might disagree with the framework about the answer, the seam carries a ladder of pre-resolution and lets the caller choose the rung — rather than a single semantic op or a single primitive one.
Q4 — command shape
Not answered here, and the reason is instructive. Java2D never reifies a drawing command. Dispatch is a virtual call on sg2d.drawpipe; the "command" is the frame on the Java stack. There is consequently no encoding to get wrong — neither a closed sum like DrawOp nor a variable-stride record, the two options F3 holds open — and, importantly, no way to record, replay, cull or diff a scene. GraphicsPrimitive.traceWrap() exists (a debug wrapper installed when traceflags != 0, GraphicsPrimitive.java) precisely because there is nothing to inspect otherwise. Java2D is the control case for F3's first half: reification is what buys those properties, and a subject that skips it has to bolt on a tracer to get any of them back.
What Java2D reifies instead is the state, not the commands: the five ordinals plus the six pipe fields are the value that would otherwise be attached to each op. That is the single most transferable idea in this subject for friction §4 and §6 — see Q6.
Q5 — sub-unit placement
Java2D's coordinates are continuous doubles in user space, so it does not meet sparkles:ui's problem in the same form. It meets the inverse problem instead — a continuous request landing on a discrete grid — which is F6's point that going continuous relocates the sub-unit question rather than dissolving it. Java2D's answer is the shape friction §5 is looking for: a named tolerance and a named minimum, never a named position.
A minimum, for dropout.
PixelToParallelogramConvertertakes aminPenSizeconstructor parameter documented as "minimum pen size for dropout control" and clamps:lw = Math.max(lw, minPenSize);.SurfaceData's static initialiser constructs the non-AA converter with1.0and the AA converters with1.0/8.0(SurfaceData.java) — the same hairline intent, spelled at two different fidelities, chosen by the framework rather than enumerated by the caller.A bounded licence to snap.
KEY_STROKE_CONTROLhas three values —VALUE_STROKE_PURE("geometry should be left unmodified and rendered with sub-pixel accuracy"),VALUE_STROKE_NORMALIZE("normalized to improve uniformity or spacing of lines"), andVALUE_STROKE_DEFAULT— and the key's javadoc bounds the damage:If an implementation performs any type of modification or "normalization" of a path, it should never move the coordinates by more than half a pixel in any direction.
The converter implements exactly that:
normalize(v)biases towardnormPosition(0.25non-AA,0.499AA), applied only whensg2d.strokeHint != SunHints.INTVAL_STROKE_PURE.
So the vocabulary is (minimum feature size, snap policy, snap tolerance). This supports F6's answer — a named fidelity plus a queried device unit, in place of RuleEdge's six compass positions — and adds that the fidelity is naturally a pair: how thin a thing may get, and how far it may move to look right.
Q6 — resolved appearance, semantic role, or both?
Neither, and this is the finding that most complicates the survey. A Java2D draw command carries no appearance at all — not resolved, not semantic. It carries geometry.
Appearance lives in the graphics state and is resolved in two stages:
- Classified into a lookup key.
SurfaceData.getPaintSurfaceType(sg2d)mapspaintStateto aSurfaceTypetoken —OpaqueColor,AnyColor,OpaqueGradientPaint,LinearGradientPaint,TexturePaint,AnyPaint— andgetFillCompositeType(sg2d)does the same for the composite. The pair plus the destination type is the registry key. This token is semantic: it names the kind of paint, not its pixels. - Resolved by whoever wins the lookup. If an accelerated loop is found, it resolves the paint on the device (
MTLPaints). If not, the general path asks the application's ownPaintobject for aPaintContextand reads rasters out of it —GeneralCompositePipe.startSequencecallssg.paint.createContext(model, devR, s.getBounds2D(), sg.cloneTransform(), hints)and then composes tile by tile.
The unresolved Paint object is thus carried all the way to the bottom, while a classification of it is carried as a lookup key. sparkles:ui puts both halves on every operation instead, and so pays for both every time: the pixels a primitive needs sit in its own payload — a 16-byte Ink on the four content primitives, and on FillRect its own colour fields plus a const(BoxChrome)* left null unless the box has a border, shadow, radius or arrow — while the classification rides along as a Slot, carried by six of the eight payloads and absent only from the clip pair. (Visual is derived from those fields on demand by DrawOp.visual, lossily and deliberately so; the seam speaks Visual end to end, and the Ink/BoxChrome split is internal to the payloads.) Java2D carries the equivalent pair once per state change, memoized in a 30-entry RenderCache keyed by (src, comp, dst) (SurfaceData.getRenderLoops).
Friction §6 records the seam hedging rather than deciding, and F9 finds that nobody else in the survey carries both on every operation. Java2D hedges too — and shows that the hedge is affordable when it is amortised over a state span rather than replicated per command. It also lands on F9's side of the derivability argument: the token that survives to the bottom is the classification, and the pixels are produced from it at the destination, never the reverse.
Q7 — payload ownership
Java (a GC'd language) removes the lifetime question that makes friction §7 sharp, but Java2D still runs into it twice, and the two answers are different:
Glyph runs are pooled and explicitly disposed.
GlyphListholds native pointers and "is not marked as finalizable since it is intended to be very lightweight"; the documented usage isGlyphList gl = GlyphList.getInstance(); try { … } finally { gl.dispose(); }.getInstance()returns one process-widereusableGLwhen a CAS on anAtomicBooleansucceeds and allocates otherwise — a single-slot pool with allocation as the contention path.The producer is pinned by a strong reference. This is the direct answer to friction §7's "record on one thread, submit on another":
A reference to the strike is needed for the case when the
GlyphListmay be added to a queue for batch processing, (e.g. OpenGL) and we need to be completely certain that the strike is still valid when the glyphs images are later referenced. This does mean that if such code discardsGlyphListand places only the data it contains on the queue, that the strike needs to be part of that data held by a strong reference.Images are cached by the destination.
SurfaceData.getSourceSurfaceDataconsults a per-destination "blit proxy cache" and may substitute a device-resident copy of a source image for the original (SurfaceData.java). This is Slint'sdraw_cached_pixmapbargain reached independently: the party that knows the payload's device lifetime owns the cache.
All three are copies, pools or strong references — no borrow of a producer's buffer survives the call — which is F8's unanimous result reached by a third route. sparkles:ui's frame arena is in that family: CmdBuffer.textRun copies the run's bytes into the arena, and the rule stated on the type is that an operation is valid while the buffer that built it is alive and unreset. What the strike passage above adds is the case the arena does not cover — a backend that records on one thread and submits on another needs the payload to travel with the operation, and that is exactly what UI-O4 leaves open.
Q8 — can a backend ask the scene its extent?
No — because Java2D keeps the three extent questions apart and answers each somewhere else. SurfaceData declares public abstract Rectangle getBounds(); and public abstract GraphicsConfiguration getDeviceConfiguration();: surface extent flows down from the destination, never up from the drawing. Layout and ink extent are answered above the seam, by TextLayout/Font.getStringBounds for text and Shape.getBounds2D() for geometry — pure model queries needing no painter, and derived by scanning the model rather than maintained as the model is built.
That is F7's three-way split, from a subject that never confuses the three: two of them come from the scene, and only the first belongs to the surface. Friction §8 is the same split left unmade: put the extent question to a finished sparkles:ui stream and none of the three places it could live answers — not the CmdBuffer that built it, not the display list, not the arena holding its text — so a backend that allocates its own surface folds op.rect over every operation to recover the one number it needs. The fold is a legitimate derived-by-scan answer; what is missing is somewhere to ask for it.
Strengths
- The fallback cannot be forgotten. Making the software implementation the superclass, and acceleration a partial override that ends in
super.validatePipe(sg2d), means every unhandled state is handled by construction. A registry-with-a-general-loop has the same property one level down:SurfaceType's "every chain ends atAny" invariant guarantees a hit. - Appearance amortised over a state span. No per-command appearance field, no per-command re-resolution. The cost of classifying paint × composite × destination is paid once per attribute change and cached.
- Measurement reified as a context value.
FontRenderContextmakes "measurement depends on the device" expressible without coupling the measurer to a live painter — measurable offscreen, cacheable, comparable byequals. - A ladder of pre-resolution for text.
String→AttributedCharacterIterator→GlyphVectorlets the caller choose how much resolution to keep for itself. - Small role interfaces, composed by adapters. Eight interfaces of 0–7 methods, and the tree is full of adapters between them (
PixelToShapeConverter,PixelToParallelogramConverter,SpanClipRenderer), so a backend implements the level it is good at.
Weaknesses
Silent degradation with no floor and no refusal. The gap between "accelerated Metal fill" and "read the destination back into a
Rasterand compose in Java" is four orders of magnitude and is crossed without a log line, an exception or an observable flag.AnyBlitandGeneralCompositePipeare both reachable from ordinary application code.Degradation policy is encoded as graph topology. Preferring a
SrcNoEaloop over aSrcOverloop is expressed by manufacturing a syntheticCompositeTypewhose supertype chain happens to enumerate the desired order:The fix is to use the following chain which looks for loops in the following order:
SrcNoEa,Src,SrcOverNoEa,SrcOver,AnyAlphaNothing prints that order; you derive it by walking
getSuperType().The contract is unreadable from one place.
validatePipeis a 175-line nested conditional;MTLSurfaceData.validatePipeis another ~100 lines that partially shadows it. The union of the two is the real behaviour and exists nowhere as a document. This is friction §2's complaint at ten times the scale.Eight bits per key component.
makeUniqueIDpacksprimTypeID, dst, comp and src into 32 bits;makePrimTypeIDthrowsInternalError("primitive id overflow")past 255. The registry is not open-ended.No reified command stream, so no recording, replay, culling, or op-stream parity testing — the properties
RecordingCanvasexists to provide, and the ones F12 identifies as reification's actual payoff.Hints are unobservable.
getRenderingHintechoes the request. An application cannot tell whether antialiasing happened.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Resolve capability per state change, not per command | Attribute changes are rare relative to draws; classify once, dispatch many | The seam's behaviour depends on invisible history; a draw call is not self-describing |
Fallback = super.validatePipe() | Structurally impossible for a backend to leave a state unhandled | The floor is silent; no caller can tell acceleration from emulation |
Loops keyed by (src, comp, dst) chains | One registry serves every device, format and blend mode; specificity is automatic | 8-bit key fields; preference order is graph topology, not data |
| A general loop synthesised on lookup miss | Correctness for every combination without an N×M×K table | The correct-but-slow path (AnyBlit, GeneralCompositePipe) is silently reachable |
Measurement via an immutable FontRenderContext | Device dependence made explicit and cacheable without a live painter | A hand-built FRC "may lead to unexpected or incorrect results" — a footgun by construction |
| Hints are advisory only | Portability across screen, image and printer destinations | No refusable degrade; no golden test can pin the fast path |
| Many small role interfaces + adapters | A backend implements the level it is good at; converters bridge the rest | The effective contract is the union of eight interfaces plus two decision trees |
| Primitive public API; widgets live in Swing | One drawing vocabulary, unlimited widget vocabularies above it | Only works because every Java2D destination can actually draw a rect |
Bearing on the proposal
Move appearance off the command and onto a state span. This is the sharpest transferable idea here, and it bears on F3 and F9 at once. F3 leaves the encoding of a reified stream a live trade — a closed sum that keeps operations comparable, against variable-stride records that pay only for what each operation uses. F9 finds that nobody carries a resolved appearance and a semantic role on every operation. Java2D suggests a move that answers both without picking a winner: a
setVisual/setSlotstate operation in the stream, with the geometry operations carrying only geometry. That drains the payloads pressing hardest against theDrawOp.sizeof <= 64budget (friction §4) and retires the per-operation hedge (friction §6) — and becausesparkles:uireifies its stream where Java2D does not, it keeps recording, replay and op-stream parity while paying appearance once per span. The price is the one the decisions table above names: an operation that carries no appearance is not self-describing, so every walker —RecordingCanvas's pairwise comparison included — has to carry the span state to interpret one.Make the fallback delegable, not just centralised.
sparkles:uialready states a degradation for each of its four optional primitives —ruleEndpointsplus a cell-alignedlineforrule,paintScrollbarCellsglyph-per-cell forscrollbar, and nothing at all forpushClip/popClip, because the display list has already culled the subtrees a clip would hide. What it does not have is Java2D's placement: those fallbacks live at the call site ininterp/immediate.d, behind__traits(compiles, …), so a backend can take them or leave them but cannot wrap or extend them.super.validatePipe(sg2d)shows the alternative — a default written in terms of the mandatory primitives that a backend delegates to. That is F5's middle rung, the defaulted one, and in D it is amixin templateof defaults rather than an interface, so it stays compatible with structural typing. Friction §2 — five methods, eight kinds — is the readability half of the same complaint.Fidelity is a pair, not a scalar. F6 answers the sub-unit question with a named fidelity plus a queried device unit. Java2D's fidelity is
(minPenSize, normPosition)plus aSTROKE_PURE/STROKE_NORMALIZEpolicy switch with a documented half-pixel bound.sparkles:ui's equivalent would be "this band is at least f of a cell thick, and you may move it up to t of a cell to make it look right" — whichGridCanvasreads as "one cell, snapped" andSkiaCanvasas "one device pixel, unsnapped", with no compass anywhere.Q1: F1 holds, and Java2D settles one of F2's six decisions.F1 puts measurement outside the painter in 35 of 38 subjects, and Java2D is one of them; friction §1's complaint is that
Size measure(const(char)[])sits onisCanvasdenominated in cells. But relocating it is only one of the six decisions F2 enumerates, and Java2D settles device parameterisation outright: the answer legitimately depends on the destination and its rendering hints, and the way to express that without coupling is a small immutable measurement context value passed to the font layer. Asparkles:uiTextMeasureshould take something FRC-shaped (cell metrics, or a scale factor plus a hinting/AA policy), not be a bareFont.F4's axis fits Java2D exactly, and it places
sparkles:uion it.F4 replaces the semantic-vs-primitive framing with a question of where the lowering lives, and Java2D is unambiguous: the lowering lives in the framework above the seam, in Swing, and the seam itself carries no semantics at all. It can afford that because every one of its destinations can draw a rectangle.sparkles:uicannot assume that of a terminal, so it cannot copy the primitivism wholesale — but it can copy the scope: keep semantic operations only where a target's fidelity floor destroys intent, which is text and hairlines.Scrollbaralready passes F4's second test, the one about derived geometry: it carriescontent,viewportandoffset, not a thumb rectangle, and every backend lowers through the singlescrollbarThumbformula insparkles.ui.stateby way ofscrollbarCellCountandscrollbarCell. What friction §3 records is that a widget concept is in the drawing seam at all — not that the seam computes its geometry.Adopt the stated floor by making degradation observable, not refusable.F5 asks for a floor plus a refusable degrade. Java2D has neither and is demonstrably worse for it — but it also shows that "refusable" is the harder half to retrofit into a portable API. The cheap first move is observability: a canvas reporting which optional primitives it actually executed, so
RecordingCanvasand golden tests can assert on it. That is F12's point in miniature — the op stream earns its keep as the parity oracle, and a degradation nobody can observe is one the oracle cannot check. Refusal can then be layered as a policy over an observable seam.Do not adopt the state-vector-as-ordinals classification. It buys Java2D a fast
<=test per capability and costs it a decision tree no one can read.sparkles:uihas a handful of canvases and eight op kinds; the pressure that producedpaintStatethroughclipStatedoes not exist here. The related discipline is worth taking instead: F11 asks that the contract be stated once and the rest derived, andOpKindalready works that way — it is an eight-armmatch!over the payload rather than a stored tag, so the kind and the payload cannot disagree the wayvalidatePipeandMTLSurfaceData.validatePipedo.
Sources
All paths verified to exist at d3e5304c0f70aa03a52f5449cb38645a184b23dc with git cat-file -e <sha>:<path> against a local clone of openjdk/jdk; the revision is that clone's HEAD (commit dated July 10, 2026).
java/awt/Graphics2D.java— the public seam: coordinate spaces, the four-phase rendering process, the three text-argument rungsjava/awt/RenderingHints.java— hints are advisory;KEY_STROKE_CONTROLand its half-pixel normalization boundjava/awt/FontMetrics.java— integer metrics,myFRC(Graphics), the "advance is not the sum of advances" notejava/awt/font/FontRenderContext.java— measurement parameterised by devicesun/font/FontDesignMetrics.java—MetricsKey(Font, FontRenderContext)cachesun/font/GlyphList.java— pooled payload, explicitdispose(), strong strike reference for queued backendssun/java2d/SunGraphics2D.java— the state vector and pipe fieldssun/java2d/SurfaceData.java—validatePipe,getMaskFill,getRenderLoops,makeRenderLoops,getBounds, the blit proxy cachesun/java2d/loops/SurfaceType.java— the chain-to-Anyinvariantsun/java2d/loops/CompositeType.java—OpaqueSrcOverNoEaand the topology-encoded preference ordersun/java2d/loops/GraphicsPrimitive.java—makeUniqueID,traceWrapsun/java2d/loops/GraphicsPrimitiveMgr.java—locatePrim's triple supertype walk and the general-loop fallbacksun/java2d/loops/Blit.java—makePrimitive,AnyBlitsun/java2d/pipe/PixelToParallelogramConverter.java—minPenSize,normalizesun/java2d/pipe/LoopPipe.java,ValidatePipe.java,NullPipe.java,GeneralCompositePipe.java— the pipe layersun/java2d/pipe/PixelDrawPipe.java,ShapeDrawPipe.java,TextPipe.java,ParallelogramPipe.java,CompositePipe.java,LoopBasedPipe.java— the role interfacessun/java2d/metal/MTLSurfaceData.java— override-and-delegate, and the "more general software-based loop" comment