Software GDI rasterizer design
Status
Implementation started 2026-08-12. WAT now owns canonical bitmap and window
surfaces, GDI object/DC state, regions and explicit clipping, native-format
pixel access, all ROP2/ROP3 truth tables, source and stretch blits, flood fill,
and integer geometry. JavaScript retains presentation uploads/composition.
Installed Win16/Win9x FNT bitmap strikes and scalable glyf TrueType faces are
parsed, selected, measured, and rasterized directly into the canonical WAT
surface without a Canvas glyph or destination readback.
The checked-in application corpus now has complete GDI dispatch coverage:
239 distinct imports from 313 PE files are public and WAT-dispatchable, with
no missing handlers or crash_unimplemented entries. Remaining fidelity work
is DBCS/complex shaping, remaining enhanced and classic metafile record
families, and printer integration. Real
Win98 rejects GGO_BEZIER; it is not a compatibility target. The checked-in
PE corpus has a machine-checked public API inventory in
gdi-public-api-status.json; its exact sorted import-set hash
prevents new application dependencies from silently expanding the
compatibility surface. All explicit gaps in that inventory must be closed
before this effort can be declared complete.
Rectangular HRGN ownership and Boolean algebra now run in WAT. WAT allocates
generation-tagged handles and owns normalization, mutation, offset, bounding
boxes, object typing, lifetime, and RGN_AND/OR/XOR/DIFF/COPY. Each
region has a fixed canonical arena of up to 208 sorted, disjoint half-open band
rectangles. Boolean results are constructed in alias-safe WAT scratch buffers.
JavaScript receives those rectangles only for window-shape presentation data;
Canvas text receives a bounded copy of the already-combined effective bands.
Ellipse regions use deterministic integer
pixel-center scan conversion into the same WAT arena. Polygon regions use an
exact-rational WAT scanline with half-open edges and grouped crossings for both
ALTERNATE and WINDING fill modes. The current explicit envelope is 208
vertices, 4,096 rows, and coordinates within +/-1,000,000; calls outside it
fail instead of delegating geometry to JavaScript.
Application-defined DC clips and USER-derived visible regions are WAT-owned in
separate tables. Each live HDC entry holds a
private canonical HRGN copy, so deleting or modifying the caller's selected
region cannot change the DC. SelectClipRgn, ExtSelectClipRgn,
IntersectClipRect, ExcludeClipRect, OffsetClipRgn, GetClipRgn,
GetClipBox, PtVisible, and RectVisible operate on the effective
intersection. Region selection remains in documented device units, while
logical rectangle, offset, visibility, and bounding-box APIs transform at the
call boundary. Retained clips therefore do not move when an application later
changes its mapping mode. Client, whole-window, nested ancestor,
WS_CLIPCHILDREN, and WS_CLIPSIBLINGS visibility contributes an independent
system region; GetClipRgn continues to expose only application state. Canvas
text consumes the same effective device bands as WAT geometry. DC deletion and
release destroy both private regions. SaveDC/RestoreDC use a WAT-owned nested
stack and take independent copies of the selected explicit clip along with the
complete hot DC record, auxiliary text/brush/arc state, color adjustment, and
selected logical palette. Absolute and relative restore indices discard the
restored snapshot and every newer snapshot.
Path brackets now have WAT-owned per-DC lifecycle and typed geometry storage.
BeginPath, EndPath, AbortPath, CloseFigure, and GetPath operate on a
bounded, growable point/type stream; points are transformed into device space
when recorded and inverse-transformed through the current mapping when queried.
MoveToEx, LineTo, Rectangle, Polygon, Polyline, PolylineTo,
PolyBezier, PolyBezierTo, PolyDraw, and PolyPolyline record instead of
painting while a bracket is open. Ellipse and RoundRect add closed
four-cubic figures with explicit straight side records, and Arc/ArcTo
split at quadrant boundaries into bounded cubic segments. Arc direction,
ArcTo's connector and current-position update, and device-space retention
are WAT-owned. AngleArc preserves its connector, current-position update,
signed sweep, and multiple revolutions; Chord and Pie close their arc with
the secant or center radials and share the same path fill/stroke consumers in
both direct and bracketed modes. FlattenPath converts cubic controls with
bounded adaptive de Casteljau subdivision and a half-pixel device-space
tolerance. Both straight and cubic figures
feed PathToRegion and SelectClipPath through canonical WAT regions, and
PathToRegion consumes the closed path as Win32 specifies. FillPath,
StrokePath, and StrokeAndFillPath use the selected WAT brush/pen and
canonical surface directly; the combined operation closes open figures and
all successful consumers discard the path. Preflighted stroke coverage and
device-space filling ensure mapping changes after EndPath do not move the
retained geometry. Text from an installed FNT strike records each covered
bitmap pixel as a closed device-space figure. Scalable TextOut,
ExtTextOut, and DrawText consume deterministic WAT-rasterized glyph strikes;
WAT converts covered pixels into retained path geometry and owns explicit
Dx/ETO_PDY advances, layout, and TA_UPDATECP. WidenPath
flattens curves, derives analytic pixel-center coverage for geometric pen
bodies, flat/round/square caps, and round/bevel/miter outer joins, then replaces
the retained path atomically with non-overlapping band rectangles. Cosmetic
wide-pen compatibility retains its captured integer coverage. Direct geometric
LineTo uses the same analytic segment/cap rule and writes each destination
pixel once for ROP2 safety. No implemented shape or text-path API falls back
to Canvas geometry or destination pixel readback.
Printer DCs no longer alias the screen surface. CreateDCA/W and the common
print dialog allocate an independent 2400x3150 32-bpp WAT bitmap for the
300-DPI printable area of Letter paper. Each successful StartPage clears the
canonical page to white; geometry, blits, bitmap-font text, and scalable text
then commit to those same bytes. Job ordering and teardown are WAT-owned.
Persisting every completed page and handing a spool/export stream to the
browser remain printer-integration work rather than GDI raster ownership work.
Classic metafile recording is also canonical WAT raster work. CreateMetaFile
allocates a bounded 640x480 software surface; CloseMetaFile serializes its
pixels into a standard top-down 32-bpp META_STRETCHDIB WMF record, and
PlayMetaFile validates, decodes, maps, clips, and presents that record through
the shared WAT blitter. This gives independently transportable WMF bytes and
faithful replay of all drawing that reached the recording surface. Native
PlayEnhMetaFile playback owns a bounded DWORD handle table, composes the
requested destination rectangle into private mapping state, and restores the
complete caller DC on every exit. EMR_STRETCHDIBITS uses the shared
validation and blitter. Core EMR mapping/color/ROP2/fill/stretch/save/clip
state, pen/brush creation-selection-deletion, SETPIXELV, current-position
lines, rectangles, ellipses, round rectangles, polygons, polylines, and
polyline-to records use the same WAT object, DC, region, and integer-raster
paths as public GDI. Both POINTL and bounded signed POINTS variants are
supported, and an entire polyline is preflighted before its first pixel.
GetWinMetaFileBits and SetWinMetaFileBits convert supported bitmap records
by replaying through a temporary canonical surface and serializing the target
standard format. Classic WMF playback also interprets common mapping, color,
ROP2, fill-mode, SaveDC/RestoreDC, pen/brush/font lifetime, line, rectangle,
ellipse, round-rectangle, polygon, polyline, META_TEXTOUT, and
META_EXTTEXTOUT records directly in WAT. Classic Region Objects are parsed
from bounded Win16 scan bands into canonical HRGNs. META_FILLREGION,
META_FRAMEREGION, META_INVERTREGION, and META_PAINTREGION share the
public WAT region raster paths; META_SELECTCLIPREGION,
META_INTERSECTCLIPRECT, META_EXCLUDECLIPRECT, and META_OFFSETCLIPRGN
retain mapped device-space clips with normal SaveDC/RestoreDC isolation. Region
handles follow the bounded WMF object table's lowest-free-slot and deletion
rules. Win16 LOGFONT, text state,
length-padded strings, ETO_OPAQUE/ETO_CLIPPED rectangles, and signed Dx
arrays are validated and converted before replay. Installed FNT faces use the
same WAT bitmap rasterizer as public text APIs and do not invoke Canvas. Its
object table is bounded by mtNoObjects; playback restores the caller DC and
deletes all surviving temporary objects on success or failure. EnumMetaFile
walks the validated record stream through a resumable guest MFENUMPROC, with
a live bounded HANDLETABLE, EOF delivery, early-stop semantics, and caller-DC
restoration. PlayMetaFileRecord routes each callback record through the same
WAT evaluator so state and object-table mutations persist between callbacks.
Enhanced-metafile font/text, region-object, path, world-transform, arc/Bezier,
multi-poly, palette, and remaining bitmap-record families are still separate
compatibility work. Unsupported classic WMF families also remain incomplete.
CreateCompatibleBitmap DDBs now use private 32-bpp, top-down canonical storage
in the WAT bitmap arena while preserving BITMAP.bmBits == NULL. Canvas remains
a derived cache: WAT raster writes only union a dirty rectangle in the canonical
surface. Memory DCs perform no Canvas conversion. Attached window, desktop, and
menu surfaces convert the accumulated rectangle immediately before compositor
use; explicit Canvas reads and captures are presentation synchronization
boundaries only. Deletion returns the private pages to the arena.
LoadBitmapA/W now materialize uncompressed BITMAPCOREHEADER and
BITMAPINFOHEADER resources plus bounded BI_RLE4/BI_RLE8
RT_BITMAP resources entirely in WAT. Encoded, absolute, end-of-line,
end-of-bitmap, and delta commands decode into canonical packed scanlines with
strict source/destination bounds. The raster core reads and writes 1-, 4-,
8-, 16-, 24-, and 32-bpp surfaces, resolves indexed colors from each bitmap's
owned RGBQUAD table (expanding core RGBTRIPLE palettes), and performs cross-format ROP blits before uploading the
result. This is sufficient for Paint's compressed tool strip and SkiFree's
indexed sprite-atlas construction, as well as Solitaire's CARDS.dll core
bitmaps. At 16 bpp, BI_RGB owns the Win32 RGB555 masks while
BI_BITFIELDS validates and retains arbitrary contiguous, non-overlapping
channel masks such as RGB565. Those masks drive resource creation, DIB
sections, pixel access, cross-format blits, geometry, and the derived Canvas
presentation cache. Logical palettes are canonical
WAT GDI objects with per-DC selection, mutation, resizing, nearest-color
lookup, and owned entry storage. DIB_PAL_COLORS bitmap creation and transient
DIB calls resolve WORD indexes through the selected logical palette; pattern
brushes preserve the indexes and resolve them against the destination DC at
sample time.
Application-supplied 12-byte BITMAPCOREINFO is normalized at the same WAT
boundary. CreateDIBitmap and CreateDIBSection expand RGBTRIPLE tables into
the bitmap's canonical RGBQUAD storage, while transient DIB drawing and
SetDIBits use a bounded scratch expansion. GetDIBits writes the legacy
16-bit dimensions and planes/depth fields and converts indexed palettes back
to RGBTRIPLE entries. Core-header pixels remain bottom-up and never pass
through Canvas.
LineTo now uses a WAT Bresenham kernel for solid pens up to 64 pixels wide on
16-, 24-, and 32-bpp DIB sections and software-backed compatible bitmaps. WAT owns logical-to-device mapping, endpoint exclusion,
canonical clip tests, all 16 ROP2 Boolean modes, native BGR byte writes, and
dirty bounds. WAT resolves selected objects and DC state into a canonical
descriptor; JavaScript only uploads the resulting dirty rectangle to the
presentation Canvas. Microsoft documents that LineTo
excludes the specified centerline endpoint and that CreatePen
uses logical width and normalizes dash styles wider than one to solid. The
reusable native Win98 DIB probe in
tools/v86-reference/probes/gdi-wide-lines.c supplies the pixel behavior that
those API contracts do not define. Captured axis-aligned width-2..5 strokes
now use exact rectangular coverage, including the observed coverage beyond
both centerline endpoints, clipping, and one write per pixel for every ROP2.
Diagonal wide strokes and widths above five retain the deterministic integer
square-stamp approximation under R2_COPYPEN; their authentic masks are kept
as non-exact fixtures for the next geometry slice. Non-copy diagonal or larger
wide operations and transformed wide operations fail explicitly pending
coverage-mask and geometric-path kernels. Client and whole-window DCs now resolve to one
top-level, WAT-owned 32-bpp backing surface with descriptor origins for child,
client, and non-client coordinates. One-pixel dash, dot, dash-dot, and
dash-dot-dot pens use fixed
device-step WAT coverage tables. CreatePen dash/dot styles wider than one are
normalized to solid as Win32 specifies.
Solid, null, and all six Win32 hatch brushes are canonical WAT objects.
Rectangle, ellipse, round-rectangle, polygon, FillRect, and region fill paths
sample hatch masks in device coordinates, including SetBrushOrgEx, opaque
background color, and transparent background preservation. GetObjectA/W
serializes stable LOGPEN, LOGBRUSH, BITMAP, and LOGFONT structures.
Pattern-dependent PatBlt, BitBlt, StretchBlt, StretchDIBits, and flood
fill operations use the same coordinate-aware sampler. CreatePatternBrush
snapshots any canonical bitmap into brush-owned storage, and
CreateDIBPatternBrushPt copies packed DIB_RGB_COLORS data or preserves
DIB_PAL_COLORS logical indexes in the same native-format bitmap records.
Pattern pixels repeat in device coordinates with the canonical brush origin
and are sampled by every brush-dependent WAT path. Palette-index patterns are
resolved through each destination DC's selected logical palette when sampled.
GetDIBits preserves the caller's declared output dimensions when a pixel
buffer is present. WAT constructs the native partial-scan intersection in
bottom-origin StartScan units, converts only the intersecting canonical
rows, and places or zero-fills the band according to the requested top-down or
bottom-up DIB orientation. The returned scan count is the intersected source
height and biSizeImage continues to describe the complete declared output
DIB. No Canvas readback participates in bitmap extraction.
SetDIBits performs the inverse declared-source intersection: StartScan
aligns the source DIB against the canonical destination bitmap, top-down and
bottom-up sources retain their native placement rules, and only intersecting
rows are written and uploaded. Its return value remains the accepted source
line count even when destination clipping reduces the number of changed rows.
Tabbed text now uses the same canonical DC-to-text binding as ordinary text.
WAT parses ANSI or UTF-16 tab characters, measures individual runs, expands
default, repeating single, or explicit multiple tab stops relative to the tab
origin, applies TA_UPDATECP, and constructs the packed width/height result.
Canvas remains responsible only for measuring and rasterizing each glyph run,
with the resulting pixels copied back to the authoritative WAT surface.
Polyline and PolylineTo reuse the WAT line kernel after an atomic
all-segment preflight. Cosmetic style phase continues across segment
boundaries and each shared endpoint is covered once. Polyline preserves the
DC current position; PolylineTo starts from and advances the WAT-owned
current position. Unsupported paths fail explicitly; there is no Canvas
geometry fallback.
This document describes the incremental migration from Canvas 2D vector drawing to deterministic software rasterization implemented primarily in WAT. It does not propose replacing Canvas as the desktop compositor or presentation API.
The immediate drivers are visible in Win98 Paint:
- Canvas vector paths antialias lines, curves, ellipses, and rounded corners.
imageSmoothingEnabled = falseaffects scaled images, notstroke()orfill()rasterization, so output differs from classic integer GDI and can differ between browser engines. - The former
CreateDIBSectionCanvas shadow required guest stores, host GDI operations, and presentation to synchronize two pixel copies. Canonical WAT memory has replaced that model. - Paint's brush-options panel is still visually incorrect. Its small bitmap and mask operations need exact pixel tests as part of this migration; software line rasterization alone will not necessarily fix it.
Decision
GDI surfaces will use one authoritative pixel store. WAT owns GDI handles, DC state, regions, clipping, rasterization, ROPs, and native-format pixel access. GDI primitives read and write the authoritative store without delegating their semantics to JavaScript. JavaScript receives dirty rectangles and remains responsible for browser-facing presentation, final window composition, scaling, and display.
Canvas vector paths must not be used for GDI primitives whose Win32 result is
defined as raster pixels. Installed FNT faces and scalable glyf faces use
WAT-owned metrics and deterministic glyph data. DBCS and complex shaping must
fail or use an explicit compatible engine, never the host's Canvas text rules.
JavaScript host imports are limited to facilities WebAssembly cannot directly
provide: Canvas upload/composition, audio, input,
storage, and similar platform APIs. Canvas may also remain an explicitly
documented compatibility fallback during migration, but new GDI algorithms
must not be implemented in JavaScript.
This is a staged replacement, not a flag-day rewrite.
Invariants
- Every bitmap or window surface has exactly one authoritative pixel store.
- A DIB section's guest-visible bytes are its authoritative store. There is no second authoritative RGBA copy.
- Canvas content is a derived presentation cache. Code never reads Canvas to discover the current value of a software-backed GDI surface.
- Each GDI write presents an explicit bounded rectangle. Guest CPU writes to DIB memory need no write barrier because all GDI reads use those bytes directly; a later explicit presentation operation uploads them to Canvas.
- GDI read/modify/write operations run against the authoritative store,
including raster operations, flood fill,
GetPixel, and source blits. - Rasterization uses integer coordinates and deterministic coverage rules. The same input must produce the same pixels in Safari, Chromium, and Node.
- HDC state remains separate from surface storage. Multiple DCs may select the same bitmap and must observe each other's writes immediately.
Surface model
Introduce a WAT-owned surface descriptor table below DC resolution. It exposes bulk operations rather than a host call for every pixel.
{
id,
width,
height,
format, // bgra32, bgr24, rgb565, indexed8, mono1, ...
stride,
topDown,
storage, // guest memory view or host-owned typed array
storageOffset,
palette,
dirty,
presentation // optional Canvas cache; never authoritative
}
There are two storage classes:
- DIB surface. References bytes in WASM linear memory allocated by
CreateDIBSection. It preserves the DIB's native bit depth, stride, palette, channel order, and top-down/bottom-up orientation. - Host surface. Owns a packed typed-array buffer for compatible bitmaps, window backing stores, printer pages, and other non-guest-visible targets. BGRA32 or RGBA32 is acceptable initially, provided conversions are explicit at the presentation boundary.
A memory DC has no pixels of its own. Its selected bitmap resolves to a surface. A window DC resolves to the window backing surface plus an origin and clip. DirectDraw surface DCs resolve through the same descriptor interface while preserving the DirectDraw surface's native storage.
Recommended resolution API:
gdi_resolve_target(hdc) -> {
surface,
originX,
originY,
clipRegion,
dcState
}
The WAT rasterizer resolves this target once per GDI call. Hot loops then operate directly on linear memory and spans without repeatedly looking up the HDC or crossing the JS boundary.
Bitmap construction uses a separate pure parsing layer in
src/10a-gdi-bitmap.wat. It validates raw BITMAPINFO/RT_BITMAP bytes,
computes bounded palette and pixel spans, decodes bounded RLE4/RLE8 streams,
plans WORD-aligned CreateBitmap
storage, initializes the canonical 48-byte bitmap record, and writes Win32
BITMAP query structures. The parser never allocates a handle or asks
JavaScript to create a GDI object. Registry binding supplies canonical storage
and copies the validated spans in a later step.
Implementation boundary
The end-state ownership is:
WAT
GDI handle allocation and object tables
DC selection, mapping, colors, modes, and clip state
DIB and host-surface descriptors
HRGN bands and Boolean operations
geometry coverage, pixels, spans, ROP2/ROP3, flood fill
dirty page/rectangle bookkeeping
JavaScript
allocate/resize Canvas presentation caches on WAT request
upload dirty native pixels to Canvas
composite windows and handle browser scaling
rasterize font glyphs into scratch masks until a WAT font backend exists
browser APIs: audio, MIDI, input, storage, clipboard, networking
The migration may temporarily mirror existing JS GDI handles into WAT tables, but each milestone must move ownership toward WAT. A JS helper that implements new region, clipping, geometry, pixel, or ROP semantics is out of scope.
Enforced JavaScript bridge
The flag-day bridge purge makes the end-state boundary executable now, even before every WAT replacement exists. Non-text GDI calls without a WAT implementation fail through explicit WAT stubs; they do not regain behavior by crossing into JavaScript. Temporary application regressions are preferable to silently preserving two semantic implementations.
The permanent presentation-only gdi_* JavaScript imports are:
gdi_set_region_bands upload canonical WAT bands to a derived clip mirror
gdi_set_window_rgn apply that mirror during browser window composition
gdi_surface_create allocate a derived Canvas cache for WAT surface metadata
gdi_surface_attach attach a WAT window/overlay surface to its compositor Canvas
gdi_surface_upload upload dirty authoritative pixels to Canvas
gdi_surface_delete discard the derived Canvas cache
No gdi_* text-policy import remains. DrawText, TextOut, and ExtTextOut
layout, tabs, ellipsis, alignment, explicit advances, prefix removal,
underlines, colors, background mode, clipping, rasterization, and destination
writes are WAT-owned. There is no current
gdi_* resource exception. LoadBitmapA/W resolve raw RT_BITMAP bytes through
the WAT PE-resource walker, validate and copy pixels and RGBQUADs into owned
canonical storage, then publish only a derived surface presentation.
test/test-gdi-migration-status.js hard-codes this allowlist, verifies that
JavaScript exports no other gdi_* methods, checks the zero temporary-exception
budget, inventories each WAT unsupported stub, and compiles the module. The
exception ceiling may decrease but must never increase.
DIB ownership and synchronization
For a DIB section, guest memory becomes the only pixel truth:
- Guest x86 stores write the DIB bytes directly.
- Host GDI primitives selected into the DIB write those same bytes.
- GDI reads consume those same bytes.
- Presentation converts changed native-format rows into a Canvas upload.
This removes:
- Canvas-to-DIB reconciliation;
- full-surface hashes used to detect which copy changed;
- ambiguity about whether a guest store or a host draw wins;
- full-bitmap conversion before a GDI read.
It removes guest-write dirty tracking. A raw store through the pointer returned
by CreateDIBSection changes canonical memory but does not itself display the
bitmap. A later BitBlt, StretchBlt, SetDIBitsToDevice, text operation, or
explicit presentation reads the current bytes and supplies the destination
rectangle to upload. CPU scalar/string/FPU stores therefore need no DIB range
checks or page notifications.
The dedicated DIB arena retains a 4 KB occupancy bitmap and allocation-run table solely for allocation and reuse. These are not presentation state. GDI operations may provide exact dirty rectangles as upload hints; JS does not infer them by watching memory.
The Canvas cache may be discarded and rebuilt at any time. Deleting the DIB reclaims its arena pages and its presentation cache together.
Rasterizer organization
Implement the rasterizer as format-neutral coverage generation plus format-specific pixel/span operations.
Coverage and geometry
- Integer line walker with Win32-compatible endpoint rules.
- Rectangle edges and fills using half-open bounds.
- Polygon edge table with
ALTERNATEandWINDINGfill modes. - Integer ellipse and arc rasterization with explicit bounding-box semantics.
- Rounded rectangles composed from spans and integer ellipse quadrants.
- Cubic Bezier flattening with a deterministic integer error bound, followed by the line walker.
- Cosmetic pen widths, styles, joins, caps, and brush patterns added in stages.
Do not assume textbook Bresenham output automatically matches GDI. Endpoint exclusion, ellipse bounds, wide-pen centering, dash phase, and polygon edge ownership must be locked with reference fixtures.
Pixel and span operations
Each supported format supplies optimized operations such as:
readPixel(x, y)
writePixel(x, y, color, rop2)
writeSolidSpan(y, x0, x1, color, rop2)
copySpan(dst, src, count, rop3)
expandMonoSpan(...)
The first implementation may specialize common Paint paths:
- 1, 4, 8, 16, 24, and 32 bpp DIB access;
- solid cosmetic pens;
- solid brushes;
R2_COPYPEN;SRCCOPY,SRCAND,SRCPAINT,SRCINVERT,PATCOPY,BLACKNESS,WHITENESS, andDSTINVERT.
Unsupported combinations must use a named, instrumented compatibility fallback or fail visibly in tests. They must not silently draw with different semantics.
Clipping
Convert the active DC clip and window visibility clip to sorted horizontal spans or rectangle bands before rasterization. Intersect generated spans with the clip bands. This avoids Canvas clip-path behavior and supports exact dirty bounds.
The existing region representation can remain initially if it can enumerate rectangles deterministically. A banded region representation is the preferred long-term form because fills, blits, and upload bounds all consume spans.
WAT now owns canonical band regions for rectangle combinations,
scan-converted polygons and ellipses, application-defined HDC clips, and the
independent USER-derived visible region. Raster and scalable text composition consume
the same target-bounds AND application-clip AND system-clip intersection.
SaveDC/RestoreDC snapshots the application clip by value; the system region
is re-derived from current window state.
Presentation
Each surface maintains an optional Canvas presentation cache. Flushing a dirty surface performs:
- Coalesce explicit dirty rows, tiles, or rectangles.
- Convert native pixels and palettes to RGBA for only those areas.
- Upload without interpolation using
putImageData,ImageBitmap, or an unscaleddrawImagefrom a staging canvas. - Mark the uploaded areas clean.
Window surfaces use the same rule. WAT owns a persistent surface record keyed
by the top-level HWND and recreates its linear-memory backing when renderer
geometry changes. gdi_surface_attach binds only the derived Canvas; bounded
uploads schedule normal renderer composition. Releasing an HDC drops its DC
record but keeps the window pixels, while destroying the owning HWND releases
both canonical pages and the presentation cache.
DirectDraw GetDC binds the synthetic HDC directly to the existing native
surface bytes in DX_OBJECTS. WAT GDI reads and writes that native format;
ReleaseDC deletes only the transient Canvas presentation and DC state. The
old DIB-to-Canvas-to-DIB round trip no longer exists, so stale Canvas pixels
cannot overwrite a DirectDraw surface.
GetDC(NULL) and GetDC(GetDesktopWindow()) select a persistent screen-sized
32-bpp WAT bitmap. Its Canvas is attached as the renderer's desktop base layer.
Normal presentation remains the renderer's efficient derived Canvas composite.
Only an API that reads a screen DC materializes the final z-order image in the
WAT bitmap. The renderer already owns the only global view of windows from
separate processes, so it copies their canonical surface bytes directly across
WASM memories in z-order. The 32-bpp path uses bulk typed-array rows; uncommon
indexed and true-color formats use the canonical software decoder. No Canvas
destination is sampled. A layout-and-surface signature caches the materialized
snapshot until a window moves, changes z-order, or uploads new pixels.
Popup menus use the same surface model rather than a semantic renderer
fallback. WAT owns a screen-sized bitmap selected into a persistent memory DC,
paints menu chrome in desktop coordinates, and calls gdi_surface_attach with
the compositor-overlay target. The renderer composites only the popup's dirty
rectangles, so opaque native bitmap storage does not cover unrelated desktop
pixels. Scalable menu glyphs use the normal WAT synthesized-strike provider;
no JavaScript menu geometry or text implementation remains.
The renderer then composes window caches onto the desktop Canvas as it does today. Browser zoom or CSS scaling may affect display size but cannot change the underlying GDI pixels.
Never read the presentation Canvas in GDI code. Text rasterization is WAT-owned. Screen-source reads consume the same canonical surface memories that feed presentation, not their Canvas caches.
Text
Text has one destination pipeline. Installed FNT 2.x/3.x resources enter it as
native bitmap strikes; scalable glyf TrueType faces are parsed, flattened,
scan-converted, cached, and exposed as synthetic FNT 3.00 strikes. The selected
font object determines the producer, while every layout, path, clipping, ROP,
and destination operation remains shared WAT code:
TrueType tables (`cmap`/`glyf`/`hmtx`/`kern`)
|
v
WAT quadratic outline loader + deterministic scan converter
|
v
exact-size synthetic FNT strike / glyph cache
|
v
shared TextOut/ExtTextOut/DrawText/path policy
|
v
authoritative WAT path or GdiSurface + dirty rectangle
Browser font availability and Canvas rasterizer differences therefore cannot move guest layout or pixels. Current limitations are emulator features rather than host variance: the runtime does not execute TrueType bytecode hinting, ANSI DBCS conversion is incomplete, and complex-script reordering/ligation is not implemented.
High-fidelity Win98 text backend
The deterministic bitmap-font path is implemented in 10b-gdi-font.wat
without changing GDI callers. AddFontResourceA reads NE .FON containers or
standalone FNT 2.x/3.x resources through the VFS boundary, validates their
tables, and copies accepted strikes into WAT-managed memory. CreateFontA/W
and CreateFontIndirectA/W bind the closest installed strike by face and
height. Measurement, metrics, TextOutA, ExtTextOutA/W, and DrawTextA/W
then use the one-bit vertical-strip glyphs and write pixels through the
canonical, format-aware GDI raster path. The WAT ExtTextOut path applies
ETO_OPAQUE, ETO_CLIPPED, horizontal or ETO_PDY paired advances, and
TA_UPDATECP before bounded presentation. The WAT DrawText path handles
explicit lines, word wrapping, horizontal and vertical alignment, rectangle
clipping, DT_CALCRECT, mnemonic prefixes and underlines, expanded tabs, and
single-line or final-visible-row end/path/word ellipsis. DrawTextExA/W
additionally applies validated DRAWTEXTPARAMS left/right margins, its separate
tab-cell interval, and uiLengthDrawn output without encoding the tab count
over unrelated format bits. DT_MODIFYSTRING copies an ellipsified byte-oriented
presentation back to ANSI or UTF-16 caller storage. The same provider-neutral
layout serves scalable faces. Its 65,540-WCHAR buffer keeps mnemonic flags in
a separate WAT byte array so UTF-16 code units remain intact.
Inside a path bracket, both installed and synthesized strikes append exact
closed pixel figures without changing destination pixels. A later path
consumer supplies the brush, pen, clipping, and raster operation.
The target selection order is:
- An exact bitmap strike from a legally supplied Windows
.FON/.FNTfile. - A deterministic exact-size strike rasterized from a bundled open substitute.
- A deterministic exact-size strike rasterized from a guest-installed TTF.
- The bundled bitmap UI fallback for an unnamed or unavailable face.
The project-native WAT loader understands NE .FON containers with one or
more RT_FONT resources as well as standalone .FNT files. Node and browsers
therefore consume the same validated bytes and integer raster algorithm.
FreeType remains a possible future dependency for PCF, BDF, CFF, bytecode
hinting, and broader font-table coverage.
For each face/size/weight/charset strike, cache:
glyph bitmap (1 bpp)
A/B/C widths or left bearing + advance
ascent, descent, internal/external leading
average/max width, first/last/default/break character
character-set/code-page mapping
kerning pairs where the source supplies them
Font matching should reproduce the GDI inputs that affect CreateFont and
LOGFONT: face aliases, height versus cell height, width, weight, italic,
underline, strikeout, charset, pitch/family, escapement, and orientation.
Point sizes must use the em-height and device DPI rules rather than CSS pixels.
The browser and CLI preload tracked System.fon, MSSansSerif.fon,
Fixedsys.fon, Courier.fon, and Terminal.fon files into
C:\\WINDOWS\\FONTS; the WAT backend installs each lazily on the first matching
text operation. The first four are generated from Wine's embedded bitmap
strikes. Terminal is generated from ANAKRON's OFL-licensed native 8x12 bitmap,
maps bytes through CP437, and declares OEM_CHARSET. SYSTEM_FONT uses System,
ANSI_FIXED_FONT uses Courier, SYSTEM_FIXED_FONT uses Fixedsys,
OEM_FIXED_FONT uses Terminal, and the variable UI stocks use MS Sans Serif.
Common Win9x UI aliases use deterministic MS Sans Serif. Explicit document
faces such as Arial resolve to license-safe open substitutes mounted under the
filenames Win98 used, while a guest-installed face takes precedence.
GetGlyphOutlineA derives metrics and DWORD-aligned monochrome GGO_BITMAP
payloads from selected FNT strikes without binding Canvas. Scalable faces also
emit transformed quadratic GGO_NATIVE contours and deterministic byte-per-
pixel GGO_GRAY2_BITMAP, GGO_GRAY4_BITMAP, and GGO_GRAY8_BITMAP coverage.
The gray maxima 4/16/64, DWORD row padding, short-buffer behavior, and
GGO_UNHINTED acceptance are tested. Affine MAT2 transforms apply to native,
monochrome, and gray output. Real Win98 returns GDI_ERROR for GGO_BEZIER,
including on Arial, so the emulator does too.
GetFontData exports bounded selected-font data; unsupported provider/table
combinations retain the documented GDI_ERROR result.
Character-width queries share one WAT font-provider boundary for installed and
scalable strikes. GetCharacterPlacementW keeps Latin ordering, widths, caret
positions, classes, maximum-extent truncation, and simple justification in WAT;
with GCP_USEKERNING, classic TrueType kern format-0 pairs adjust the left
glyph's lpDx, caret, and packed extent exactly as captured on Win98. The
deployed CP1252 subsets preserve those legacy tables even when the source also
contains modern GPOS. DBCS expansion and complex-script shaping remain future
work rather than a geometry or destination-surface fallback.
Font enumeration is also provider-aware. EnumFontsA and the ANSI/Unicode
EnumFontFamilies variants retain scalable Arial as the first compatibility
face, then invoke the guest callback for every unique installed FNT family.
WAT constructs each ENUMLOGFONTEX and NEWTEXTMETRICEX from parsed strike
metadata, applies family and charset filters, and resumes or terminates through
a stack-resident callback context without consulting Canvas.
The fixed stock objects therefore retain distinct native roles instead of
sharing an outline-derived substitute: Courier 8x13 for ANSI_FIXED_FONT,
ANAKRON-derived Terminal 8x12 for OEM_FIXED_FONT, and Wine Fixedsys 8x15 for
SYSTEM_FIXED_FONT. Integer-scaled bitmap strikes handle larger matching
requests without antialiasing. Their measurement and glyph writes stay entirely
on the WAT pixel surface.
Raster output should be monochrome by default for the Win98 look. Glyph origins
and advances are integers; TA_UPDATECP, alignment, inter-character spacing,
justification, tabs, opaque backgrounds, ETO_CLIPPED, and ETO_OPAQUE are
handled above the font provider. Single-line overflow and the final vertically
visible multiline row support end, word, and path ellipsis in that WAT layout
layer. Complex scripts require an explicit Win98-compatible shaping engine and
language-edition fixtures; host Canvas shaping is not a compatibility path.
Validation requires reference captures from an actual Win98 environment at
known DPI. Test GetTextMetrics, GetTextExtentPoint32, ABC widths, dialog
layout, menu widths, edit caret placement, underline/strikeout, and exact glyph
bitmaps for the stock UI and fixed fonts. Pixel hashes should match across
Safari, Chromium, and Node when the same deterministic strike is selected.
Font sources and licensing
The deterministic stock resources have pinned, redistributable inputs:
- Wine System, MS Sans Serif, Fixedsys, and Courier preserve Wine's native embedded monochrome strikes and remain under LGPL-2.1-or-later.
- ANAKRON-derived Terminal preserves ANAKRON v0.3.3's native 8x12 BDF pixels, is renamed for the compatibility role, and remains under OFL-1.1.
- W95FA and Fixedsys Excelsior remain legacy shell CSS assets; they no longer generate or back the covered stock GDI bitmap resources.
Generated strikes remain subject to their source font's license and
attribution/renaming requirements. Deterministic output does not make an open
substitute's artwork identical to Microsoft's original. The complete candidate
comparison, including ReactOS, unix4lyfe CP437, Kakwa, Terminus, W95FA, and
Fixedsys Excelsior, is in
bitmap-font-review.md; pinned sources, hashes, and
license copies are recorded under fonts/.
Do not bundle original Microsoft SSERIFE.FON, VGASYS.FON, VGAOEM.FON,
Tahoma, Microsoft Sans Serif, or extracted bitmap strikes without a verified
redistribution license. Converting a proprietary font to BDF, PNG, or a custom
atlas does not remove its license restrictions. Exact original fonts should be
loaded only from user-provided files or from a licensed redistributable package.
Every bundled font or generated strike must have an upstream URL, version,
license text/SPDX identifier, checksum, and generation recipe recorded under
fonts/.
Migration plan
Phase 0: lock current behavior
- Add a Paint test for the brush and airbrush options panel. Verify the number, placement, selected border, and masks of all option glyphs.
- Add a diagonal-line assertion that permits only foreground and background colors. It must fail on intermediate antialiasing colors.
- Extend the checked-in native Win98 wide-line fixtures beyond the current axis and geometric-cap cases with multi-segment join and representative ROP captures; keep the probe source reusable in the repository.
- Capture reference fixtures for rectangles, ellipses, curves, and polygons in Node and the browser harness.
- Preserve the existing all-tools, file round-trip, dirty-New, large-scroll, flood-fill, and airbrush-position tests.
Phase 1: surface abstraction
- Add
GdiSurfaceand make compatible bitmaps and DIB sections register one. - Route
GetPixel,SetPixel, solid fill, and clear operations through it. - Retain the current Canvas as a presentation cache and compare pixels after every migrated operation in a debug dual-run mode.
Exit gate: migrated reads never call CanvasRenderingContext2D.getImageData
for authoritative bitmap content.
Phase 2: blits and raster operations
- Move
BitBlt,StretchBlt,PatBlt, mono expansion, and Paint's mask paths to surface-to-surface operations. - Support overlap-safe self-blits and all currently exercised ROPs.
- Diagnose the Paint brush-options panel with exact source/destination surface dumps; fix it before declaring this phase complete.
Exit gate: Paint tool icons, option glyphs, palette, and selection masks pass pixel fixtures in Node and browser tests.
Phase 3: integer drawing primitives
- Migrate
MoveToEx/LineTo, polyline, rectangle, polygon, ellipse, round-rectangle, arc, and Bezier calls. - Add pen width/style and brush fill incrementally, driven by executable tests.
- Move flood fill to direct surface access and remove its Canvas readback.
Exit gate: Paint drawing contains no unintended intermediate colors, and the same operation fixtures hash identically in Safari, Chromium, and Node.
Phase 4: window and DirectDraw surfaces
- Make window backing stores authoritative software surfaces.
- Resolve transient, synthesized, and allocated HDCs through the same target model.
- Integrate DirectDraw
GetDCwithout DIB-to-Canvas-to-DIB round trips.
Exit gate: GDI code reads no window or bitmap presentation Canvas for state. Text may read only its temporary scratch Canvas before committing to the authoritative surface.
Phase 5: remove shadow synchronization
- Delete DIB hashes and Canvas-to-DIB conversion.
- Delete compatibility paths whose callers have migrated.
- Retain bounded presentation uploads without guest-write page tracking.
- Keep Canvas vector GDI fallbacks removed.
Testing strategy
Use three layers:
- Rasterizer unit tests: small buffers with exact expected byte arrays for endpoints, clipping, orientation, stride, palette lookup, and ROP truth tables.
- GDI API tests: create/select surfaces through host imports and verify DC state, aliasing, deletion, overlapping blits, and dirty rectangles.
- Executable tests: Paint and other real binaries exercise operation sequences, resource masks, window invalidation, and presentation.
Cross-engine browser tests should compare logical surface pixels or PNG hashes, not screenshots after CSS scaling. Safari remains an important presentation check because it exposed the original Canvas geometry and coordinate differences.
Reference fixtures should record their source and environment. Prefer captures from an actual Win98 system or a documented compatible GDI implementation. Do not encode Canvas output as the expected result for operations being migrated.
Performance constraints
npm run bench:gdi runs the public-handler WAT benchmark used by the unit
suite. It warms each workload, reports the median of at least three samples,
and covers SetPixel, line and rectangle rasterization, PatBlt, BitBlt,
StretchBlt, and the stock bitmap-font TextOut fast path. Each case checks its
resulting pixels and has a deliberately broad minimum throughput gate. The
gate catches catastrophic routing or upload regressions; the printed medians,
not the gate values, are the useful numbers for comparing implementations on
the same machine.
- Use typed-array loops over contiguous spans, not per-pixel object allocation.
- Specialize solid fills and common ROPs; bulk operations should dominate Paint workloads.
- Coalesce each surface's dirty bounds and convert them once at compositor, capture, or Canvas-text synchronization boundaries. An unattached memory DC must not perform presentation conversion.
- Keep the current direct guest-memory translation fast path. DIB stores need no instrumentation because the DIB bytes are canonical.
- Profile before moving code into WASM. JavaScript typed arrays may be adequate for normal Win98 resolutions; the surface contract should permit a later WASM rasterizer without changing callers.
Risks
- Exact GDI semantics are broader than Paint. Metafiles, world transforms, geometric pens, palette realization, printer DCs, and uncommon ROPs cannot be treated as incidental variants.
- Native-format DIB writes complicate every primitive. A temporary RGBA authoritative buffer would be simpler but would recreate synchronization for guest-visible DIBs, so format adapters are required.
- Window-surface conversion changes invalidation and child-window clipping. It should follow bitmap migration, not lead it.
- TrueType bytecode hinting, DBCS conversion, and complex shaping are explicit fidelity boundaries; they must not silently inherit a host font engine.
- A software rasterizer can regress performance if it uploads whole canvases or uses generic per-pixel dispatch in hot loops.
Non-goals
- Replacing the desktop/window compositor with a software framebuffer in the first migration.
- Implementing the entire documented Win32 GDI API before landing useful phases.
- Replacing the HTML shell's CSS font rendering; guest GDI text is WAT-owned.
- Treating antialiasing removal alone as proof of GDI compatibility.
- Making raw
CreateDIBSectionstores immediately visible without an explicit GDI transfer or presentation operation.