API — ICoreEssentials/UI/Backends/WinUI/Painting
The public contract of 7 header(s) under ICoreEssentials/UI/Backends/WinUI/Painting — 14 class/struct definition(s), 73 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.
ICoreWinUIPaintAccess.h#
ICoreEssentials/UI/Backends/WinUI/Painting/ICoreWinUIPaintAccess.h
Backend-internal access to what an ICorePen, ICoreBrush or ICoreGradient actually says.
⚠ THIS HEADER IS THE ANSWER TO §35.1, AND IT IS WHY W1.5 CAN NOW START. That section found the ordering blocker:
ICorePenandICoreBrushhave no getters at all. Their entire public surface is constructors, setters, andnativeStorage()-- and on the Qt backend the state crosses the seam as the native object (icoreQt(pen)reinterprets the buffer as aQPen&), so a WinUIICorePainter::Implhanded anICorePencould not ask it anything. The only way in wasnativeStorage(), which holds aQPen, and naming aQPeninsideBackends/WinUI/is exactly whatR2.4exists to stop.So this backend takes its own side of the seam, as W1.6 did for the five value types it owns: the buffer holds a pointer to a record THIS backend
ICoreWinUIPaintStop#
ICoreWinUIPaintAccess.h:41 · struct · 0 declaration(s)
One stop of a gradient, in the units ICoreWinUIPainter::GradientStop takes.
struct ICoreWinUIPaintStop {
public:
double position = 0.0; // 0..1
ICoreColor color;
};
};
ICoreWinUIBrushSpec#
ICoreWinUIPaintAccess.h:59 · struct · 0 declaration(s)
struct ICoreWinUIBrushSpec {
public:
ICoreWinUIBrushKind kind = ICoreWinUIBrushKind::None;
ICoreColor color; // Solid
// LinearGradient: the two ends, in the CURRENT transform space, like
// QLinearGradient and like ICoreWinUIPainter::setBrushLinearGradient.
double x1 = 0.0, y1 = 0.0, x2 = 0.0, y2 = 0.0;
// RadialGradient: centre and radius. ⚠ ONE radius, not two. The painter's
// call takes radiusX and radiusY because Direct2D's ellipse gradient has
// both; `ICoreRadialGradient` only ever states one, so a seating passes it
// twice rather than inventing an ellipse nothing asked for.
double centerX = 0.0, centerY = 0.0, radius = 0.0;
// ⚠ SORTED BY POSITION, because that is what QGradient::stops() answers and
// what a gradient brush needs; the order they were SET in is not preserved
// by either backend.
std::vector<ICoreWinUIPaintStop> stops;
};
};
ICoreWinUIPenSpec#
ICoreWinUIPaintAccess.h:84 · struct · 0 declaration(s)
⚠ A PEN CARRIES A BRUSH, NOT A COLOUR, and that is not over-modelling: ICorePen(const ICoreBrush&, double) exists and its one call site (ICoreNotificationCenterMessageBox's rim) passes a gradient.
struct ICoreWinUIPenSpec {
public:
ICoreWinUIBrushSpec brush;
double width = 0.0;
ICorePenStyle style = ICorePenStyle::Solid;
ICorePenCap cap = ICorePenCap::Square;
ICorePenJoin join = ICorePenJoin::Bevel;
bool cosmetic = false;
};
};
File-scope declarations#
// Which of the painter's three brush calls this brush wants.
//
// ⚠ `None` IS NOT `Solid` WITH ALPHA 0, and the difference is load-bearing:
// `ICoreBrush()` means "do not fill at all" -- fifteen call sites spell it as a
// bare `ICoreBrush()` to turn filling off before stroking an outline (see
// ICoreBrush.h) -- and it maps to `setNoBrush()`, not to a transparent fill.
enum class ICoreWinUIBrushKind {
None = 0,
Solid = 1,
LinearGradient = 2,
RadialGradient = 3,
};
ICoreWinUIPainter.h#
ICoreEssentials/UI/Backends/WinUI/Painting/ICoreWinUIPainter.h
ICoreWinUIPainter#
ICoreWinUIPainter.h:37 · class · pImpl · nested GradientStop · 40 declaration(s)
The WinUI backend's drawing core, on Direct2D.
class ICoreWinUIPainter {
public:
struct GradientStop {
double position; // 0..1
int r, g, b, a; // 0..255
};
explicit ICoreWinUIPainter(ID2D1RenderTarget* target);
~ICoreWinUIPainter();
ICoreWinUIPainter(const ICoreWinUIPainter&) = delete;
ICoreWinUIPainter& operator=(const ICoreWinUIPainter&) = delete;
// ⚠ Direct2D HAS NO save/restore. It has a transform, an antialias mode and
// a clip STACK that must be popped in reverse. These two maintain the state
// stack Qt's callers assume, including unwinding exactly the clips pushed
// since the matching save() -- see the .cpp, where getting it wrong is a
// corrupted render target rather than a wrong pixel.
void save();
void restore();
// ⚠⚠ RE-ESTABLISH THE STATE THIS PAINTER STARTED WITH, FOR A NEW FRAME
// (`W10.100`). Call it once per frame, before any drawing. It unwinds any
// clips still pushed, empties the save/restore stack and puts the transform,
// antialiasing and text antialiasing back to what the render target had when
// this painter was built.
//
// It exists because THIS PAINTER OUTLIVES THE FRAME: one is built per
// surface and reused for every frame that surface survives, so a widget of
// fixed size keeps the same painter for the life of the window. The
// destructor already hands the render target back clean -- and between two
// frames the destructor does not run. Without this, a paint hook that set a
// transform and did not restore it left it set for the NEXT frame, and the
// transform calls compose, so the error was geometric rather than constant.
void beginFrame();
void setAntialiasing(bool on);
// Whether TEXT is antialiased, tracked apart from setAntialiasing because
// both toolkits track it as its own hint -- ICorePainter keeps the two
// separate for the same reason, and the print path is the caller that
// sets it.
void setTextAntialiasing(bool on);
void setOpacity(double opacity); // 0..1, multiplies every brush
[[nodiscard]] double opacity() const; // what setOpacity() last set
void setPen(int r, int g, int b, int a, double width);
void setNoPen();
// ⚠ These take the tree's OWN enums, not plain ints, and that is not a
// contradiction of the plain-numbers rule above. ICoreInputEnums.h is
// toolkit-free by construction -- the event-map tables lean on the same fact --
// so using them costs no Qt and buys the vocabulary the call sites already
// speak. It is ICorePen, not ICorePenStyle, that has to wait for the value tier.
//
// ICorePenStyle::None disables the pen outright, matching Qt::NoPen.
void setPenStyle(ICorePenStyle style);
void setPenCap(ICorePenCap cap);
void setPenJoin(ICorePenJoin join);
void setBrush(int r, int g, int b, int a);
void setNoBrush();
// x1,y1 -> x2,y2 in the CURRENT transform space, like Qt's QLinearGradient.
void setBrushLinearGradient(double x1, double y1, double x2, double y2,
const std::vector<GradientStop>& stops);
void setBrushRadialGradient(double centerX, double centerY,
double radiusX, double radiusY,
const std::vector<GradientStop>& stops);
// ⚠ A PEN PAINTS WITH A BRUSH, NOT A COLOUR, and that is not
// over-modelling: ICorePen(const ICoreBrush&, double) exists and its one
// call site in this tree passes a GRADIENT (the notification message
// box's rim). Without these two, seating that pen would flatten the rim
// to a solid with nothing to say so -- and the flattening would be
// invisible in every test that never draws that one control.
//
// The plain setPen() above stays as it is: it is what every other call
// site spells, and a solid colour is a one-stop brush nobody should have
// to write out.
void setPenLinearGradient(double x1, double y1, double x2, double y2,
const std::vector<GradientStop>& stops, double width);
void setPenRadialGradient(double centerX, double centerY,
double radiusX, double radiusY,
const std::vector<GradientStop>& stops, double width);
// Replace the ink already drawn in the box with `tint`, keeping its alpha —
// the "tint a glyph" operation. What is on the device keeps its shape and
// its soft edges; only its colour changes. ICorePainter names this rather
// than exposing a composition mode (its header explains why: ~30 toolkit
// modes, exactly one idiom in this tree), and the icon engines are the
// callers.
//
// ⚠ AXIS-ALIGNED ONLY. It operates on pixels ALREADY RASTERISED, so a
// rotated or skewed transform has no meaningful reading — see the .cpp,
// where such a transform makes this a no-op rather than something subtly
// wrong. All three call sites in the tree tint icons under translation and
// scale.
void floodKeepingAlpha(double x, double y, double w, double h,
int r, int g, int b, int a);
void drawRect(double x, double y, double w, double h);
void drawRoundedRect(double x, double y, double w, double h,
double radiusX, double radiusY);
void drawEllipse(double centerX, double centerY, double radiusX, double radiusY);
void drawLine(double x1, double y1, double x2, double y2);
void drawPolyline(const std::vector<double>& xy); // x,y,x,y...
void drawPolygon(const std::vector<double>& xy);
// Which points a fill counts as inside. The two answers differ for exactly
// the shapes vector art draws deliberately: a ring cut as two nested
// subpaths is SOLID under NonZero when both wind the same way and HOLLOW
// under EvenOdd, so a renderer that picks for the document fills in holes
// that were meant to be holes. Named here rather than taken from
// ICorePainterPath's ICoreFillRule (which it matches one for one) because
// that header carries ICore value types and this core deliberately takes
// none.
enum class FillRule { NonZero, EvenOdd };
// Path building. One path at a time; beginPath() discards any unfinished
// one, which is what makes an early return from a caller harmless.
//
// ⚠ THE RULE IS FIXED WHEN THE PATH OPENS, not when it is filled. Direct2D
// sets the fill mode on the geometry SINK and will not accept it after the
// first figure begins, so there is no setFillRule() to call later and the
// no-argument form is not a shorthand for one.
void beginPath();
void beginPath(FillRule rule);
void moveTo(double x, double y);
void lineTo(double x, double y);
void cubicTo(double c1x, double c1y, double c2x, double c2y, double x, double y);
void closeSubpath();
void drawPath(); // fill with brush, then stroke with pen
void fillPath();
void strokePath();
// ----------------------------------------------------------------------
// Deferred path operations -- a two-slot stack machine over the path above.
//
// ⚠ THESE EXIST BECAUSE ICorePainterPath's WINUI SEAT DEFERS TWO OF ITS
// OPERATIONS RATHER THAN COMPUTING THEM. `united()` is a boolean region
// union and `strokedRound()` is stroke-to-outline; Qt answers both by
// computing a new curve list (QPathClipper, QStroker -- thousands of
// lines), and Direct2D answers both natively from geometries it already
// has. So the seat records the OPERATION and the resolution happens HERE,
// at the draw call, where a device exists. See ICoreWinUIPathAccess.h.
//
// The shape is a stack because a deferred path is a TREE: a union may have
// a union for an operand. A caller walks the tree depth-first, building
// each leaf with beginPath()/moveTo()/... and calling pathPush() to set it
// aside, and each operator consumes what its operands pushed:
//
// Built beginPath(rule); ...; (leave as the current path)
// Union(a,b) emit(a); pathPush(); emit(b); pathPush(); pathUnion(rule)
// Stroked(a,w) emit(a); pathPush(); pathStrokedRound(w, rule)
//
// Each operator leaves its RESULT as the current path, so a nested operand
// is pushed by exactly the same pathPush() a leaf uses and the walk needs
// no special case at any level.
//
// ⚠ THE RESULT IS ALREADY SEALED. Direct2D geometry is immutable once
// closed, so an operator's output cannot be appended to -- drawPath(),
// fillPath(), strokePath() and setClipPath() all work on it, and any
// moveTo() after one is ignored until the next beginPath().
// Set the current path aside as an operand. A no-op when no path is open.
void pathPush();
// Pop the last TWO operands and make their union the current path; the
// one pushed FIRST is the left-hand operand. `rule` is the fill rule of
// the RESULT, which is not always either operand's -- see the seat.
void pathUnion(FillRule rule);
// Pop ONE operand and make the outline of stroking it `width` across the
// current path. ROUND caps and joins, matching ICorePainterPath's
// strokedRound(), which is deliberately not the general form.
void pathStrokedRound(double width, FillRule rule);
// ----------------------------------------------------------------------
// Text, on DirectWrite.
//
// ⚠ THE FONT IS DESCRIBED BY VALUE, not by an ICoreFont, for the same
// reason everything else here is: ICoreFont is the value tier and is still
// pImpl-over-Qt. This is the boundary the AppKit backend drew too -- the
// painter's core did CoreText RENDERING while the value tier did the font
// OBJECT, and the two rows landed independently. `family` is a UTF-16
// family name, which is what DirectWrite takes.
//
// Text is drawn with the current BRUSH, matching QPainter -- the pen has
// nothing to do with it.
// ----------------------------------------------------------------------
void setFont(const wchar_t* family, double pixelSize, int weight, bool italic);
// Anchored on the BASELINE, like QPainter::drawText(QPointF, ...).
// ⚠ DirectWrite positions a layout by its top-left, so this is not a
// forward -- see the .cpp, where the baseline is recovered from the line
// metrics.
void drawTextAtBaseline(double x, double y, const wchar_t* text);
// Laid out in a box under ICoreAlignment flags. `wrap` false clips to the
// box on one line, which is the difference the ICorePainter header keeps
// between drawText() and drawWrappedText().
void drawTextInRect(double x, double y, double w, double h,
ICoreAlignment alignment, const wchar_t* text, bool wrap);
// The advance width and height one line of `text` needs in the current
// font. Enough for the painter's own layout; ICoreFontMetrics proper is
// the value tier.
void measureText(const wchar_t* text, double& width, double& height) const;
// ----------------------------------------------------------------------
// Images.
//
// ⚠ RAW PIXELS, not an ICorePixmap, for the same reason the font is
// described by value: ICorePixmap is part of the value tier.
// AppKit drew images in its core on the same seam while the pixmap object
// and the two rows landed independently.
//
// `bgra` is premultiplied BGRA — the byte order a WIC/D2D surface uses and
// the one ICorePixmap will hand over. `stride` is bytes per row, which is
// NOT always width*4 for a real bitmap.
// ----------------------------------------------------------------------
//
// W10.131: `serial`, when not 0, names this version of the pixels
// (icoreWinUIPixmapSerial) and the GPU bitmap made from them is kept and
// reused for as long as this painter lives -- the upload was ~100 us a
// draw. 0 uploads every time, as before.
void drawPixmap(double x, double y, double w, double h,
const void* bgra, int pixelWidth, int pixelHeight, int stride,
unsigned long long serial = 0);
void translate(double dx, double dy);
void rotate(double degrees);
void scale(double sx, double sy);
// The general affine, composed on the LEFT like the three above. The six
// numbers are in ICoreScene's row-vector order (x' = m11*x + m21*y + dx),
// which is D2D's convention too, so it is a field-for-field copy.
//
// ⚠ IT EXISTS BECAUSE A SCENE ITEM'S WORLD TRANSFORM CAN SHEAR.
// ICoreWinUIScenePainter.h names this gap in its own banner and works
// around it by mapping corners itself; an item that hands paintContent()
// a painter cannot, because the subclass draws in ITS OWN coordinates and
// the painter has to be there. Decomposing into translate/rotate/scale is
// exact only with no shear, and a rotation composed with a non-uniform
// scale has shear -- so the decomposition would be right for most trees
// and quietly wrong for one class of them (W4.5).
void concatTransform(double m11, double m12,
double m21, double m22,
double dx, double dy);
// The scale at which this painter's output actually lands on DEVICE
// pixels, per axis -- the current transform's two column lengths folded
// together with the render target's DPI.
//
// ⚠ THE HYPOT IS NOT DECORATION. Taking m11 and m22 alone reports a
// rotated painter as unscaled, so a renderer rasterising vector art at
// "the size it will really be shown at" would rasterise a rotated icon at
// the wrong size and only a rotated one. ICorePainter::deviceScale() reads
// its Qt answer the same way, and this is the number it forwards.
void deviceScale(double& scaleX, double& scaleY) const;
void setClipRect(double x, double y, double w, double h);
void setClipPath(); // clips to the current built path
void clearClip();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreWinUIPainterAccess.h#
ICoreEssentials/UI/Backends/WinUI/Painting/ICoreWinUIPainterAccess.h
The one place this backend turns a painter handle back into the painter it points at. Backend-private: include it from a .cpp inside UI/Backends/WinUI/ and nowhere else.
⚠ WHY A HANDLE AND A CAST AT ALL, when both sides are this backend's own types. The seam is not between toolkits, it is between TEST COSTS. The painted surfaces here -- the button chrome, the splitter divider, the scrollbar, the menu, the spin box -- all take an
ICoreWinUIPainter&, and every one of their suites reads pixels back out of a WIC bitmap with no window, no Windows App SDK and no Qt linked. A painted SEAT, on the other hand, is handed anICorePainter&, because that is what a widget's paint hook publishes. Reaching the painters throughICorePainterinstead would pull the value tier -- and throughICoreString, Qt itself -- into suites that are cheap precisely because they link none of it. The element's
Declares no class of its own — see the file.
ICoreWinUIPathAccess.h#
ICoreEssentials/UI/Backends/WinUI/Painting/ICoreWinUIPathAccess.h
ICoreWinUIPathNode#
ICoreWinUIPathAccess.h:50 · struct · 0 declaration(s)
struct ICoreWinUIPathNode {
public:
ICoreWinUIPathOp op = ICoreWinUIPathOp::Built;
// op == Built
std::vector<ICorePathBuilderElement> elements;
// ⚠ THE FILL RULE TRAVELS WITH THE NODE, not with the path handle. A union
// of two paths has a fill rule of its own, and it is NOT always the
// receiver's -- see the suite, which diffs it against QPainterPath rather
// than assuming.
int fillRule = 1; // matches ICoreFillRule: 0 NonZero, 1 EvenOdd
// op == Union
ICoreWinUIPathRef operandA;
ICoreWinUIPathRef operandB;
// op == StrokedRound (operandA is the path being stroked)
double strokeWidth = 0.0;
};
};
File-scope declarations#
// Backend-internal access to what an ICorePainterPath actually contains.
//
// The peer of ICoreWinUIPaintAccess.h next door, and the last of W1.8's six.
// ⚠ NOTHING ABOVE THE BACKEND ZONE MAY INCLUDE THIS.
//
// ⚠ A PATH IS A TREE HERE, NOT A CURVE LIST, and that is the row's one real
enum class ICoreWinUIPathOp {
Built = 0, // the element list is the whole answer
Union = 1, // operandA ∪ operandB
StrokedRound = 2, // the outline of operandA stroked `strokeWidth` across
};
using ICoreWinUIPathRef = std::shared_ptr<const ICoreWinUIPathNode>;
ICoreWinUIRasterSession.h#
ICoreEssentials/UI/Backends/WinUI/Painting/ICoreWinUIRasterSession.h
An off-screen paint session over a raster: Direct2D drawing into a WIC bitmap whose bytes come in and go back out again.
It is the peer of ICoreWinUISurface.h next door and NOT the same thing, which is worth saying because the two look interchangeable and one substituted for the other is a silent defect either way:
- a SURFACE is owned by the element it paints. It is created once, kept
across repaints, sized in DIPs from a layout, and it CLEARS TO TRANSPARENT at the start of every pass because a widget redraws its whole ground every time.
- a SESSION is owned by nobody and lives for one paint. It is sized in
DEVICE PIXELS from a raster that already exists, and it must NOT clear, because the raster it was handed already has something in it.
ICoreWinUIRasterSession#
ICoreWinUIRasterSession.h:69 · class · pImpl · 9 declaration(s)
The session itself.
class ICoreWinUIRasterSession {
public:
ICoreWinUIRasterSession();
~ICoreWinUIRasterSession();
ICoreWinUIRasterSession(const ICoreWinUIRasterSession&) = delete;
ICoreWinUIRasterSession& operator=(const ICoreWinUIRasterSession&) = delete;
// Open a session over `widthPixels` x `heightPixels` device pixels at
// `devicePixelRatio`.
//
// `seed` is the raster's CURRENT bytes -- packed premultiplied BGRA,
// top-down, `seedStride` bytes per row, `heightPixels` rows -- and it is
// COPIED, so the caller may release it the moment this returns. Passing
// null asks for a transparent ground instead, which is what a caller with
// no raster yet wants and what no caller in this tree actually is.
//
// False means Direct2D or WIC refused and nothing may be drawn.
bool begin(int widthPixels, int heightPixels, double devicePixelRatio,
const std::uint8_t* seed, int seedStride);
[[nodiscard]] bool isActive() const;
// The drawing core aimed at the raster, as the handle ICorePainter takes.
// Null until begin() has succeeded and after end().
[[nodiscard]] void* painterHandle() const;
// Close the session and read the raster back. Idempotent -- repeats hand
// back the first result -- because the seat's end() is, and the destructor
// calls it too.
bool end();
// The drawn bytes, packed premultiplied BGRA, top-down. Empty until end()
// has succeeded.
[[nodiscard]] const std::vector<std::uint8_t>& pixels() const;
// Bytes per row of pixels(): widthPixels * 4, never padded.
[[nodiscard]] int stride() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreWinUIRichText.h#
ICoreEssentials/UI/Backends/WinUI/Painting/ICoreWinUIRichText.h
ICoreWinUIRichStyle#
ICoreWinUIRichText.h:94 · struct · 1 declaration(s)
What a stretch of text looks like.
struct ICoreWinUIRichStyle {
public:
bool bold = false;
bool italic = false;
bool underline = false;
bool mono = false;
int script = 0; // -1 subscript, 0 baseline, +1 superscript
double sizeScale = 1.0;
// ⚠ OUT OF LINE, AND THE HEADER-SURFACE CENSUS IS WHY. A one-line body
// in a header is a `[body]` violation of the pImpl format rule -- caught by
// check_header_surface.py at R4.2, which went red on exactly this
// comparison. Fix the change, not the guard: it is defined in the .cpp.
bool operator==(const ICoreWinUIRichStyle& other) const;
};
};
ICoreWinUIRichRun#
ICoreWinUIRichText.h:109 · struct · 0 declaration(s)
struct ICoreWinUIRichRun {
public:
std::string text; // UTF-8, entities already resolved
ICoreWinUIRichStyle style;
};
};
ICoreWinUIRichParagraph#
ICoreWinUIRichText.h:115 · struct · 0 declaration(s)
A paragraph: <p>, an <h3>, an <li> or the loose text between them.
struct ICoreWinUIRichParagraph {
public:
std::vector<ICoreWinUIRichRun> runs;
std::string marker; // "\xe2\x80\xa2 " or "3. ", empty if none
int indentLevels = 0; // nesting depth of the enclosing lists
bool spaceBefore = false;
bool spaceAfter = false;
};
};
ICoreWinUIRichMetrics#
ICoreWinUIRichText.h:140 · struct · 3 declaration(s)
⚠ THE MEASUREMENT IS INJECTED, AND THAT IS WHAT MAKES THE GEOMETRY TESTABLE AT ALL.
struct ICoreWinUIRichMetrics {
public:
std::function<double(const std::string&, const ICoreWinUIRichStyle&)> textWidth;
std::function<double(const ICoreWinUIRichStyle&)> lineHeight;
std::function<double(const ICoreWinUIRichStyle&)> ascent;
// One indent step, and the gap between paragraphs. Both in the same units
// as the three callbacks answer in.
double indentStep = 16.0;
double paragraphSpacing = 6.0;
};
};
ICoreWinUIRichPlacedRun#
ICoreWinUIRichText.h:151 · struct · 0 declaration(s)
struct ICoreWinUIRichPlacedRun {
public:
std::string text;
ICoreWinUIRichStyle style;
double x = 0.0;
double baseline = 0.0; // absolute, from the layout's origin
};
};
ICoreWinUIRichLine#
ICoreWinUIRichText.h:158 · struct · 0 declaration(s)
struct ICoreWinUIRichLine {
public:
std::vector<ICoreWinUIRichPlacedRun> runs;
double top = 0.0;
double height = 0.0;
};
};
ICoreWinUIRichLayout#
ICoreWinUIRichText.h:164 · struct · 0 declaration(s)
struct ICoreWinUIRichLayout {
public:
std::vector<ICoreWinUIRichLine> lines;
double width = 0.0; // the widest line actually produced
double height = 0.0;
};
};
ICoreWinUISurface.h#
ICoreEssentials/UI/Backends/WinUI/Painting/ICoreWinUISurface.h
The pixels a painted widget draws into, and the arithmetic that decides how many of them there are.
A widget on this backend paints with Direct2D and shows the result through a XAML Image, so something has to own a bitmap that both can reach: Direct2D renders into it, and the bitmap's bytes are handed to a WriteableBitmap. That is this class. It is the same WIC-bitmap render target the drawing core was proved against, which is why it needs no window and no App SDK and can be tested by reading pixels back.
⚠ THE SIZING IS THE PART THAT GOES WRONG, so it is four free functions above the class rather than four lines inside it. Every one of them is a rule with a failure that does not look like a sizing bug:
ICoreWinUISurface#
ICoreWinUISurface.h:66 · class · pImpl · 20 declaration(s)
The bitmap itself.
class ICoreWinUISurface {
public:
ICoreWinUISurface();
~ICoreWinUISurface();
ICoreWinUISurface(const ICoreWinUISurface&) = delete;
ICoreWinUISurface& operator=(const ICoreWinUISurface&) = delete;
// Make sure a surface exists that serves this size at this scale, growing
// or re-creating as the rules above require. False means Direct2D or WIC
// refused, and the caller must not paint.
bool ensure(double logicalWidth, double logicalHeight, double scale);
// Between these two, and only between them, painter() is valid.
// begin() clears to transparent, which is what a widget with no opaque
// ground expects.
void begin();
bool end();
// The same pair for a frame that repaints ONE RECTANGLE of the surface, in
// device pixels of the content extent. Only that rectangle is cleared, drawn
// into and copied out to pixels(); every other pixel keeps what the previous
// frame left there.
//
// ⚠ THE RECTANGLE IS A CLIP UNDER THE PAINTER, NOT ONE OF ITS CLIPS. The
// painter's own beginFrame(), restore() and clearClip() unwind only what
// the painter pushed, so no paint hook can pop it and draw outside the
// cleared area -- which would blend a translucent item over its own
// previous frame instead of over a cleared one.
//
// ⚠ ONLY MEANINGFUL WHILE generation() IS UNCHANGED AND THE CONTENT EXTENT
// IS THE ONE THE LAST FRAME COVERED. A re-created surface has no previous
// frame to keep, and the caller has to draw it whole; endRegion() copies the
// whole content itself if pixels() does not already cover it.
void beginRegion(std::uint32_t x, std::uint32_t y, std::uint32_t width, std::uint32_t height);
bool endRegion();
// Counts the times ensure() has had to create a new surface. Pixels from an
// earlier frame exist only while this is unchanged.
[[nodiscard]] std::uint64_t generation() const;
// W10.130 -- draw on the GPU instead of into a WIC bitmap. Takes effect at
// the next ensure(), which re-creates the surface if the answer changed.
//
// A hardware surface is a Direct2D device context on the process's shared
// D3D11 device, drawing into a GPU bitmap that keeps its pixels between
// frames exactly as the WIC bitmap does -- so beginRegion()/endRegion()
// mean what they always meant. What it does NOT do is copy those pixels to
// the CPU: the seat presents hardwareTarget() GPU to GPU, and pixels()
// reads them back only when someone asks, which is a snapshot and not a
// frame.
//
// ⚠ IT FALLS BACK RATHER THAN FAILING. No GPU device, or one that refuses
// the bitmap, and ensure() builds the WIC surface instead and says true;
// hardware() reports which one it got.
void setHardware(bool wanted);
[[nodiscard]] bool hardware() const;
// The GPU bitmap a hardware surface draws into (an ID2D1Bitmap1*), and the
// device it lives on (an ID2D1Device*). Null for a WIC surface. Only a
// device context on that same device may read the bitmap.
[[nodiscard]] void* hardwareTarget() const;
[[nodiscard]] void* hardwareDevice() const;
// How many times the shared GPU device has been lost and rebuilt. A frame,
// or anything presented from one, belongs to the device of its epoch; a
// surface notices the change at its next ensure() and is re-created.
[[nodiscard]] static std::uint64_t hardwareDeviceEpoch();
// Report that the shared GPU device is gone -- a present that failed with
// DXGI_ERROR_DEVICE_REMOVED, say. The next ensure() builds a new one.
static void hardwareDeviceLost();
// The drawing core aimed at this surface. Null before ensure() succeeds.
//
// ⚠ RETURNS THE CORE, NOT AN ICorePainter. The seat builds the wrapper from
// this handle for the length of one paint -- the wrapper is a stack object
// by design and the core outlives it.
[[nodiscard]] void* painterHandle() const;
// ⚠ THE PIXELS THE CURRENT SIZE COVERS, NOT THE BITMAP THAT WAS ALLOCATED.
// ensure() keeps an oversized surface across a shrink (see
// icoreWinUISurfaceStillFits), so the two differ for as long as a widget
// stays smaller than its high-water mark -- and a caller that showed the
// allocation would scale the frame down by the shrink ratio. These, and
// pixels() below, all describe the same content rectangle.
[[nodiscard]] std::uint32_t widthPixels() const;
[[nodiscard]] std::uint32_t heightPixels() const;
// The bytes, BGRA premultiplied, top-down, packed at widthPixels() * 4.
// Empty until end() has succeeded.
//
// ⚠ ON A HARDWARE SURFACE THIS READS THE GPU BACK, once per frame asked
// about -- a stall, which is fine for a snapshot and wrong for a frame. The
// seat's own present never calls it on one.
[[nodiscard]] const std::vector<unsigned char>& pixels() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};