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:

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

  1. Every bitmap or window surface has exactly one authoritative pixel store.
  2. A DIB section's guest-visible bytes are its authoritative store. There is no second authoritative RGBA copy.
  3. Canvas content is a derived presentation cache. Code never reads Canvas to discover the current value of a software-backed GDI surface.
  4. 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.
  5. GDI read/modify/write operations run against the authoritative store, including raster operations, flood fill, GetPixel, and source blits.
  6. Rasterization uses integer coordinates and deterministic coverage rules. The same input must produce the same pixels in Safari, Chromium, and Node.
  7. 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:

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:

This removes:

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

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:

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:

  1. Coalesce explicit dirty rows, tiles, or rectangles.
  2. Convert native pixels and palettes to RGBA for only those areas.
  3. Upload without interpolation using putImageData, ImageBitmap, or an unscaled drawImage from a staging canvas.
  4. 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:

  1. An exact bitmap strike from a legally supplied Windows .FON/.FNT file.
  2. A deterministic exact-size strike rasterized from a bundled open substitute.
  3. A deterministic exact-size strike rasterized from a guest-installed TTF.
  4. 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:

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

Phase 1: surface abstraction

Exit gate: migrated reads never call CanvasRenderingContext2D.getImageData for authoritative bitmap content.

Phase 2: blits and raster operations

Exit gate: Paint tool icons, option glyphs, palette, and selection masks pass pixel fixtures in Node and browser tests.

Phase 3: integer drawing primitives

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

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

Testing strategy

Use three layers:

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.

Risks

Non-goals