Non-GDI Work Plan

Last updated: 2026-08-12.

Status: planning baseline for work that can proceed while the software-GDI rewrite is owned separately. This plan covers the currently known open work in WordPad/RichEdit/OLE, common controls, IME, thread fidelity, selected app bring-up, and supporting test infrastructure. Historical app notes remain useful evidence, but an old hypothesis is not an active bug until a current-tip probe reproduces it.

The software-GDI implementation itself is intentionally out of scope. Work in this plan must not change rendering merely to make a screenshot pass.

Boundary

                              CAN PROCEED NOW

  guest/API state       COM/OLE lifetime       loaders/decoders
  messages/commands     persistence bytes      registry/VFS
  text readback         input sequencing        app control flow
  test observability    performance counters    nonvisual invariants
           \                   |                    /
            +------------------+-------------------+
                               |
                               v
                      STATE-COMPLETE FEATURES
                               |
                +--------------+---------------+
                |                              |
                v                              v
       remain state-testable          wait for GDI rejoin gate
                                      pixels, metrics, shaping,
                                      exact clipping/composition

In scope now

Park until the GDI rejoin gate

Global execution rules

  1. Reproduce current-tip behavior before promoting a historical note into the active queue.
  2. Add state assertions before implementation. Screenshots may be captured for later comparison but are not a pass condition in this plan.
  3. Put a timeout only around commands that launch the emulator. Default to 90 seconds per emulator child; split multi-stage tests so each child has its own bound.
  4. Builds, unit tests that do not launch the emulator, and Git commands do not need a timeout.
  5. Preserve CRLF and Unicode semantics deliberately. Do not build a knowingly broken ANSI-only or LF-only intermediate format.
  6. Keep commits small and scoped. Do not use git reset; stage only files that belong to the current slice.
  7. Update the relevant app/design status document in the same slice as each feature or newly proven blocker.
  8. Do not weaken a visual regression to a state-only test. Split it into a state gate that can pass now and a retained visual gate for the GDI rejoin.

Recommended order

P0  current-tip audit and state-test harness
 |
 +--> P1 WordPad thread-path proof
 |
 +--> P2 OLE storage/stream completeness
 |       |
 |       v
 |    P3 multi-format IDataObject and clipboard
 |       |
 |       v
 |    P4 persistent/general OLE objects
 |       |
 |       +--> P5 links, activation, and drag/drop
 |
 +--> P6 RichEdit semantic breadth and international state
 |
 +--> P7 common-control state fidelity
 |
 +--> P8 app bring-up tracks (AoE, MCM, screensavers)
 |
 v
P9  cross-app nonvisual regression and documentation
 |
 v
GDI REJOIN: retained pixel, metric, layout, and shaping gates

P1, P6, P7, and the audit portions of P8 do not depend on the OLE chain. Within OLE, storage and multi-format transfer should precede general object activation so later code has one ownership model instead of special cases.

First implementation queue

Use these as commit-sized milestones. Do not begin a later OLE milestone while an earlier ownership/persistence contract is still ambiguous.

  1. Thread trace: generic event log plus bounded WordPad startup test.
  2. Stream core: complete on 2026-08-12. IStream::Clone, independent cursors, shared backing lifetime, and edge-case coverage pass 23/23.
  3. Storage tree: complete on 2026-08-12. Nested trees, mutations, enumeration, transactional checkpoints, and complete bounded STATSTG metadata pass 68/68.
  4. CFB persistence: deterministic writer, defensive reader, and fresh-process tree comparison.
  5. Multi-format transfer: in progress. Format collection, full enumerator, and caller-owned GetDataHere media are complete; Unicode text conversion, exact negotiation, clipboard snapshots, and remaining lifetime cases follow.
  6. Generic persistence: storage-backed object record and complete IPersistStorage lifecycle, retaining unknown children.
  7. Synthetic OLE server: lifecycle, cache, advisory, clipboard, and verb tests with no external app dependency.
  8. Links and target drag/drop: moniker/ROT state, registration ownership, effect negotiation, and WordPad logical insertion.
  9. RichEdit breadth: semantic RTF/large-document fixtures, then Unicode and IME logical state.
  10. Control state: WordPad ruler/commands, then advanced ListView messages and notifications.
  11. App audit batch: AoE, MCM, screensavers, Pinball, RCT, Abe, Tour, Spider, SkiFree, and Calculator; promote only current reproduced failures.
  12. Cross-app state regression: execute P9 and update status documents, leaving the visual half intact for the GDI rejoin.

P0 — Current-tip audit and state-test harness

Work

Acceptance

P1 — Thread fidelity and WordPad startup proof

Status: complete on 2026-08-12. The scheduler models CREATE_SUSPENDED, suspend counts, and ResumeThread, and the focused WordPad regression proves the real startup path.

Work

  1. Add bounded thread-event observability containing thread handle, creation flags, suspend count before/after resume, and first runnable slice.
  2. Add test/test-wordpad-thread-startup.js using the real WordPad binary.
  3. Assert that a suspended worker cannot execute before the matching resume, ResumeThread reports the previous count, and the worker becomes runnable only when the count reaches zero.
  4. Keep test/test-thread-manager.js as the lower-level contract suite and add pending-thread, invalid-handle, multiple-suspend, and termination regressions only if the app trace exposes a gap.

Acceptance

Implementation result: the host receives dwCreationFlags as part of the thread-creation call, so CREATE_SUSPENDED is atomic. ThreadManager exposes structured create, suspend, resume, spawn, first_run, and exit events. test/test-wordpad-thread-startup.js passes 8/8 and proves suspended create -> final resume -> spawn -> first runnable slice at the original entry point.

P2 — Structured storage and stream completeness

The existing bounded implementation supports binary ILockBytes, root IStorage, named stream children, and class identity. Complete the reusable storage layer before adding more OLE object types.

P2.1 Stream semantics

2026-08-12 stream-core result: clones retain a canonical root stream, share its mutable data/size/capacity, and keep their own positions. Write and SetSize are visible across interfaces, shrink/grow zero-fills exposed bytes, HGLOBAL clones return the same handle, and a clone remains readable after the original caller and owning storage are released. test/test-ole-storage.js passes 27/27 after the first storage-tree slice.

2026-08-12 stream-completion result: buffered CopyTo handles exact and partial 64-bit counts plus safe self-copy; shared-root checkpoints restore bytes/size through any clone; root-visible region locks distinguish write/exclusive access, enforce exact-owner unlock, gate resize, and disappear with their owner. The combined storage/stream suite passes 68/68 after the metadata completion described below.

P2.2 Storage tree

2026-08-12 nested-storage result: storage nodes now keep separate first-child and next-sibling links, so arbitrary depth does not corrupt sibling traversal. Parents own children, retained children survive ancestor release, names are case-insensitive, and streams/storages share one collision namespace. The public CreateStorage/OpenStorage handlers use the same helpers as the 27/27 focused storage suite. Rename/delete then raise it to 33/33: both element types rename without identity changes, cross-type collisions fail, unlinking removes lookup visibility, and retained streams/subtrees stay valid after deletion. Deep CopyTo and identity-preserving MoveElementTo raise the suite to 41/41. Copies own independent stream bytes and nested storage nodes; moves reject cycles and collisions before unlinking, and STGMOVE_COPY retains the source. Snapshot IEnumSTATSTG raises the suite to 47/47. Next returns exact partial counts and caller-owned names; records preserve names, types, stream sizes, and storage CLSIDs even after the live tree is renamed, deleted, and released. Skip, Reset, and cloned independent cursors are covered through a generated seven-method COM vtable. Deep transaction checkpoints raise the suite to 53/53. Commit stages an independent tree before atomically replacing the prior checkpoint; Revert stages restoration before swapping live contents. Names, bytes, nested nodes, and CLSIDs restore, post-commit additions disappear, old retained interfaces detach safely, and later commits replace earlier checkpoints.

Completing bounded STATSTG metadata raises the suite to 68/68. IStream, IStorage, and ILockBytes now share one 72-byte record implementation with correct type, low/high size, name ownership, STATFLAG_NONAME, stream lock capabilities, storage CLSID, and masked state bits. Enumerator snapshots retain the same metadata, while deep copy and transaction checkpoints preserve state bits.

P2.3 Compound File Binary persistence

Status: bounded CFB v3-on-ILockBytes persistence complete on 2026-08-12. Path-backed StgCreateDocfile/StgOpenStorage remain a separate filesystem integration task.

2026-08-12 persistence result: the in-memory tree serializes to a valid CFB v3 container with a 512-byte header/sectors, DIFAT/FAT chains, directory sectors, name-ordered red-black sibling trees, CLSIDs/state bits, regular stream chains, and mini-stream/mini-FAT chains for data below 4 KiB. The independent byte-level suite in test/test-ole-cfb.js passes 22/22. It checks Microsoft CFB name ordering, deterministic bytes, nested parentage, exact small/large payloads, fresh-backed reopen, public IStorage::Commit emission, and atomic rejection of bad signatures, sector cycles, directory cycles, illegal names/colors, and unsupported 64-bit sizes. StgOpenStorageOnILockBytes now invokes that defensive reader when no live root is associated.

Acceptance

P3 — General IDataObject and clipboard transfer

The core object now owns multiple formats/media and exposes stable snapshot enumerators. General clipboard conversion and drag/drop need the remaining transfer breadth below.

Work

2026-08-13 transfer result: IDataObject owns a growable collection of deep-copied FORMATETC/STGMEDIUM entries. SetData appends or replaces matching formats, honors fRelease, and retains stream/storage media with COM ownership; DVTARGETDEVICE data is copied independently. IEnumFORMATETC captures stable deep snapshots and implements exact multi-entry Next/Skip/Reset/Clone behavior. GetDataHere fills caller-owned HGLOBALs without replacing them, rejects undersized buffers before writing, rewrites caller streams exactly, and atomically replaces caller storage trees through a detached staging copy. The focused suite passes 28/28, while the existing static-handler, storage, and CFB suites remain green at 13/13, 68/68, and 22/22.

2026-08-13 negotiation result: format lookup now narrows clipboard format, aspect, lindex, target-device bytes, and compatible media masks in a stable order and returns DV_E_FORMATETC, DV_E_DVASPECT, DV_E_LINDEX, DV_E_DVTARGETDEVICE, or DV_E_TYMED as appropriate. Distinct presentations no longer overwrite each other, and enumeration advertises the concrete medium each entry can actually return. The expanded focused suite passes 39/39.

2026-08-13 text-transfer result: public SetData canonicalizes HGLOBAL text to Windows CRLF, publishes independently owned CF_TEXT, CF_OEMTEXT, and CF_UNICODETEXT values, preserves exactly one terminating NUL, and leaves registered RTF as an opaque coexisting format. Unicode input retains UTF-16 code units while the bounded ANSI/OEM fallback maps unrepresentable units to ?. The three-format replacement is failure-atomic through a preflighted ownership-moving collection rebuild. The focused suite passes 45/45. The WordPad rich clipboard test passes 21/21 and the Paint clipboard test passes 9/9 after restricting Paint's delayed bitmap materialization to mspaint.exe.

2026-08-13 clipboard-snapshot result: OleFlushClipboard now replaces a local owner with a distinct data-object value snapshot. HGLOBAL bytes, IStream backing, recursive IStorage trees, target-device metadata, and format enumeration remain independent if the former owner mutates or adds data. The bounded external Paint path wraps its already rendered CF_DIB in a local data object. The focused suite passes 50/50.

2026-08-13 guest-media teardown result: final IDataObject and static-handler cache destruction now scan all transferred media, validate every DLL-private Release callback before mutation, and resume asynchronous guest callbacks in entry order. Stream/storage interfaces release before pUnkForRelease; an HGLOBAL delegated to a guest releaser remains untouched by the runtime. The guest callback suite passes 39/39, the data-object and static-handler suites remain green at 55/55 and 65/65, and WordPad static-DIB Copy/Cut/Paste remains green at 13/13. Mutation-time cleanup is covered by the following result.

2026-08-13 guest-media mutation result: IDataObject::SetData and IOleCache::SetData move displaced guest media into a temporary owned data object, commit the replacement only after validating every guest Release, then reuse the suspended final-release continuation. IOleCache::Uncache uses the same path before removing an entry. Canonical text synthesis now moves unrelated guest-owned entries without a fake local AddRef, retires replaced text media, and asynchronously consumes a guest-released fRelease input. Malformed methods leave the old entry and caller medium intact. The expanded guest callback suite passes 48/48; data-object, static-handler, storage, callback-state, and WordPad static-DIB gates remain green at 55/55, 65/65, 68/68, PASS, and 13/13. DLL-private AddRef during non-transferring copies and snapshots is the next ownership slice.

2026-08-13 guest-GetData result: public IDataObject::GetData now detects a DLL-private IStream/IStorage presentation, validates its guest AddRef, and suspends the API frame until the x86 callback returns. The output STGMEDIUM is published only after the independent receiver reference exists and uses a NULL pUnkForRelease, so ordinary ReleaseStgMedium balances that reference without affecting the stored custom releaser. A missing guest method returns E_NOINTERFACE with a fully zeroed output. The guest callback suite passes 52/52; data-object, static-handler, storage, callback-state, and WordPad gates remain green at 55/55, 65/65, 68/68, PASS, and 13/13.

2026-08-13 guest-SetData result: non-transferring IDataObject::SetData and IOleCache::SetData now stage DLL-private IStream/IStorage media, suspend for their real guest AddRef, and publish through the ordinary transfer path only after the new reference exists. The caller's STGMEDIUM remains unchanged. If the later mutation fails, the continuation rolls the new reference back with guest Release; successful replacement retires displaced guest media before returning. Missing callback slots and malformed prior ownership leave both object state and caller input intact. The guest callback suite passes 57/57; data-object, static-handler, storage, and WordPad gates remain green at 55/55, 65/65, 68/68, and 13/13.

2026-08-13 guest-cache-snapshot result: IOleObject::GetClipboardData now stages the complete multi-format IDataObject before invoking guest code, prevalidates every DLL-private stream/storage AddRef and matching Release, then retains each medium through the suspended x86 continuation. No output is published until every entry owns its reference; malformed later entries do not even AddRef earlier entries. The result remains independently alive after the source cache is destroyed and final release balances every retained medium. CF_DIB render-slot mirroring already deep-copies its HGLOBAL bytes and clears pUnkForRelease, so it has no interface-reference gap. The guest callback suite passes 63/63; data-object, static-handler, storage, and WordPad gates remain green at 55/55, 65/65, 68/68, and 13/13.

2026-08-13 guest-cache-import result: IOleObject::InitFromData now constructs a detached complete cache, prevalidates every imported guest AddRef/Release and every displaced guest Release, and retains the new stream/storage media before atomically swapping cache collections. The detached handler becomes the retired owner of the old collection, allowing its DLL-private media to release asynchronously after commit without exposing a half-imported cache. Malformed new or old callbacks leave the live cache and all reference counts unchanged; source and imported cache retain independent balanced ownership. The guest callback suite passes 68/68; data-object, static-handler, storage, and WordPad gates remain green at 55/55, 65/65, 68/68, and 13/13.

2026-08-13 guest-stream-flush result: public OleFlushClipboard now snapshots DLL-private IStream media by invoking the provider's real Clone, querying the clone seek position, copying its complete value into a new local stream, and restoring that logical position on the durable copy. The source stream is never repositioned. Ordinary HGLOBAL/local stream/local storage formats are staged beside it in a detached IDataObject; no format is published until the whole collection succeeds. A malformed later guest stream leaves the original clipboard owner and all earlier formats untouched. After publication, a final former owner releases its guest media through the existing suspended teardown bridge. The guest callback suite first passed 74/74, including rejection of a Clone that aliases the source seek pointer.

2026-08-13 guest-storage-flush result: DLL-private IStorage clipboard media now complete the same detached transaction through real guest Stat(STATFLAG_NONAME) and recursive CopyTo calls. The local durable storage retains root CLSID and state bits as well as independent nested storages, streams, and bytes. Provider mutation after flush cannot affect the snapshot; the original storage stays alive and unchanged until atomic publication, then the former final owner releases its guest reference through the shared bridge. The guest callback suite passes 78/78. This closes the remaining OleFlushClipboard stream/storage value-lifetime gap.

2026-08-13 medium-ownership result: transferred HGLOBAL media honor a local pUnkForRelease without freeing the delegated payload, while stream/storage media release both their interface reference and a distinct custom releaser. GetData still returns independent caller-owned copies, and successful SetData(..., TRUE) clears all caller medium fields only after ownership has transferred. At this 55/55 milestone, DLL-private releasers still needed the suspended guest callback completed by the public-release result below.

2026-08-13 public-release result: ReleaseStgMedium now suspends its API frame for DLL-private guest interfaces. HGLOBAL with pUnkForRelease preserves the delegated payload and releases only its provider; IStream/IStorage media release the data interface first and the custom releaser second, including mixed runtime-local/guest pairs. Every guest Release slot is validated before the medium is cleared, so malformed inputs remain wholly intact instead of being partially released by a void API. The guest-x86 callback suite passes 33/33, the local data-object suite remains 55/55, and native WordPad object clipboard coverage remains 13/13. Internal object-owned guest media teardown remains the next medium-lifetime slice.

Acceptance

P4 — Persistent and general OLE objects

Static CF_DIB is a useful first handler, not the general object model.

P4.1 Persistence and cache model

2026-08-13 persistence-state result: the bounded embedded handler now enforces uninitialized, normal, no-scribble, and both hands-off states. Load starts clean, InitNew starts dirty, repeated initialization returns CO_E_ALREADYINITIALIZED, and SaveCompleted validates its handoff storage. Save As recursively stages and atomically replaces the destination, preserving unknown streams, storage children, root CLSID, and state bits without needing to understand their schema. The static-handler suite passes 26/26. General multi-presentation caching and user/lifecycle metadata remain in P4.1/P4.2.

2026-08-13 cache-collection result: IOleCache::Cache now assigns stable connection IDs to distinct format/aspect presentations and suppresses matching duplicates. SetData copies or transfers independently owned media, replaces only the matching presentation, and keeps the first usable CF_DIB mirrored into the proven render path. Uncache removes only its connection and returns OLE_E_NOCONNECTION for an unknown ID. The focused handler suite passes 34/34, and WordPad inline object Copy/Cut/Paste remains green at 13/13. IEnumSTATDATA now snapshots complete format/ADVF/sink/connection fields, deep-copies target-device metadata, and supports exact Next/Skip/Reset/Clone cursor semantics. The expanded focused suite passes 38/38.

P4.2 Object lifecycle contracts

2026-08-13 clipboard-conversion result: IOleObject::GetClipboardData now returns a distinct local IDataObject containing every cached presentation, with deep FORMATETC metadata and independently owned media. IOleObject::InitFromData validates the bounded local object kind, stages all presentations, and swaps the collection only after every copy succeeds; the first usable CF_DIB is then restored into the proven render slot. Later cache replacement/removal cannot mutate an earlier clipboard result. DLL-private data objects still require the deferred guest COM callback bridge. The focused static-handler suite passes 42/42, and native WordPad object clipboard regression remains a separate acceptance gate.

2026-08-13 lifecycle-metadata result: the bounded handler now deep-copies and atomically replaces both host names, validates content aspect for extent queries, marks successful extent changes dirty, returns caller-owned Static Object user-type text, and advertises OLEMISC_RECOMPOSEONRESIZE | OLEMISC_STATIC. It records validated close options, maintains running and nested run-lock state, honors last-unlock-close, and tracks whether the object is contained. Owned host strings are released with the handler. The later guest client-site and live-advisory results below complete DLL-private SaveObject, live sink notifications, handler ownership, and guest-owned advisory enumeration snapshots. The focused static-handler suite passes 52/52 at this milestone.

2026-08-13 guest-client-site result: a stack-resident continuation context now suspends an OLE API frame, invokes DLL-private guest x86 vtable methods, and resumes the exact caller EIP/ESP after stdcall cleanup. SetClientSite AddRefs before publishing and releases the replaced site, repeated assignment is neutral, GetClientSite returns an independently AddRefed interface, and final handler destruction releases its ownership before freeing WAT storage. SAVEIFDIRTY/PROMPTSAVE Close propagates a failing guest SaveObject HRESULT without clearing dirty state and clears it after success. The real-vtable guest callback suite passes 10/10; the existing local static-handler suite remains 65/65, and native WordPad static-object Copy/Cut/Paste remains 13/13.

2026-08-13 local-advisory result: IOleObject::Advise now assigns monotonic connection IDs and retains each synthetic local sink independently. Unadvise removes only its requested connection and returns OLE_E_NOCONNECTION without mutation for an unknown ID. EnumAdvise returns a stable IEnumSTATDATA snapshot whose Next and Clone operations own local sink references and preserve independent cursors even after the live collection is changed. Final enumerator/object release balances all retained references. The focused suite passes 65/65. At that local-only milestone, DLL-private sinks remained borrowed and could not receive notifications without the guest callback bridge.

2026-08-13 guest-advisory result: IOleObject::Advise now invokes DLL-private sink AddRef, Unadvise invokes its matching Release, and final handler destruction balances every remaining guest client-site and advisory reference before freeing WAT storage. Successful dirty Close calls every live guest sink's OnSave followed by OnClose; clean and OLECLOSE_NOSAVE paths emit only OnClose, while a failing guest SaveObject preserves dirty state and suppresses both notifications. Traversal is bounded by stable connection IDs, so mutation cannot skip a following sink and newly advised sinks wait for the next sequence. At this milestone the real guest-x86 callback suite passed 18/18, with guest-owned EnumAdvise snapshot references as the next lifecycle slice.

2026-08-13 guest-EnumAdvise result: each DLL-private sink now receives a distinct guest AddRef for the IEnumSTATDATA snapshot, every returned STATDATA, and every cloned snapshot. Final source and clone release execute the matching guest Release sequence before freeing enumerator storage. Snapshots remain usable after live Unadvise and after the source enumerator is destroyed. Missing guest AddRef/Release slots fail before publishing output, advancing a cursor, or partially releasing ownership. The expanded real guest x86 callback suite passes 29/29; the local static-handler suite remains 65/65, the continuation regression passes, and native WordPad static-object Copy/Cut/Paste remains 13/13.

Acceptance

P5 — Links, activation, verbs, and drag/drop

This is the final nonvisual OLE layer. Rendering an activated object remains a GDI-rejoin concern, but its protocol and state machine can be completed now.

P5.1 Linked objects and activation

2026-08-14 file-moniker result: CreateFileMoniker now returns an independently owned kind-specific IMoniker, not the former IMalloc-vtable placeholder. QueryInterface validates the complete canonical COM IID and exposes IUnknown, IPersist, IPersistStream, and IMoniker; final Release frees the owned UTF-16 path. GetClassID returns CLSID_FileMoniker, GetDisplayName returns a fresh CoTaskMem-compatible buffer, IsEqual and Hash use matching case-insensitive/slash-neutral filename semantics, and IsSystemMoniker returns MKSYS_FILEMONIKER. Simple enumeration and immutable dirty state are defined. Binding consults the retained ROT; unsupported composition still returns explicit HRESULTs without publishing fabricated outputs. test/test-ole-moniker.js exercises the public API thunk and all 23 vtable slots and passes 32/32 after the persistence slice below.

2026-08-14 bind-context result: CreateBindCtx now creates a real 13-slot IBindCtx with BIND_OPTS, BIND_OPTS2, and BIND_OPTS3 state. Bound-object registrations retain one reference per call, revoke one matching reference, and release the complete bound set on request or final context destruction. The case-sensitive object-parameter table AddRefs values, replaces equal keys only after the new reference is owned, releases displaced values, and returns independently AddRefed results. EnumObjectParam returns a seven-slot IEnumString over an independently owned key snapshot with exact Next/Skip/Reset/Clone cursor semantics and caller-owned string buffers. Emulator-local objects use their canonical COM root while DLL-private objects run their real guest AddRef/Release methods through the suspended callback bridge. GetRunningObjectTable returns a correctly reference-counted ROT interface. test/test-ole-bind-context.js exercises the public API/vtables, options, duplicate ownership, replacement ordering, snapshots, guest callbacks, final teardown, and malformed-interface atomicity and passes 33/33.

2026-08-14 running-object-table result: the process-local ROT now retains registration records independently of wrapper lifetimes, issues stable nonzero cookies, compares file monikers by normalized value, and balances local or DLL-private object ownership across registration, lookup, and revocation. Duplicate registrations remain distinct and return MK_S_MONIKERALREADYREGISTERED; timestamps support NoteChangeTime and GetTimeOfLastChange. EnumRunning returns a stable seven-slot IEnumMoniker snapshot with caller-owned results and independent cloned cursors. File-moniker IsRunning, GetTimeOfLastChange, and BindToObject use the shared ROT, and successful binding also registers the returned object with the supplied bind context. test/test-ole-running-object-table.js exercises the public APIs, all ROT and enumerator slots, duplicate and wrapper lifetime, local and guest ownership, timestamps, snapshots, and binding and passes 28/28.

2026-08-14 persistence result: file-moniker IPersistStream::Save now writes the interoperable XP-era payload rather than a private emulator blob: the ANSI path record, 0xDEADFFFF marker and padding, plus the tagged exact UTF-16 path when ANSI would be lossy or the value names a directory. Load consumes the same bounded record incrementally, validates every length/tag/read count, and does not replace the live moniker value until a complete new path is owned. GetSizeMax returns the conservative Windows estimate. Local emulator streams run synchronously; DLL-private streams run their real Read/Write methods through the suspended callback continuation. The expanded moniker regression locks down exact bytes, ANSI and Unicode round trips, guest callbacks, and atomic malformed-input failure and passes 32/32. This closes the current OLE milestone; composition, activation, verbs, links, and drag/drop remain deferred while work returns to GDI fidelity.

P5.2 Drag/drop

Acceptance

P6 — RichEdit semantic and international breadth

The current app-level suite already covers representative advanced RTF, printing, layout stress, ruler/dialog commands, Unicode input, and version differences. Expand semantic coverage without conflating it with exact layout.

P6.1 RTF semantics

P6.2 Large-document and version behavior

P6.3 International logical behavior

Acceptance

P7 — Common-control state fidelity

P7.1 WordPad UI state

P7.2 Advanced ListView

Acceptance

P8 — App bring-up independent of GDI

These tracks begin with a current-tip audit because their documents contain long superseded investigation histories.

P8.1 Age of Empires

P8.2 Motocross Madness / D3D call path

P8.3 Screensavers and DirectAnimation

P8.4 Task Manager

P8.5 Other documented app gaps

These are real non-GDI candidates recorded in current app documents, but they rank below the shared foundations above. Audit each on current tip before implementation because several files retain superseded debugging chronicles.

Acceptance

P9 — Integration and documentation

GDI rejoin gate

When the software-GDI work is stable, do not reopen every feature design. Run the retained visual half of this plan:

  1. WordPad static-DIB fresh-process reopen and delete/reopen pixels.
  2. WordPad toolbar/menu/ruler geometry, icon states, text metrics, wrapping, selection, caret, and clipping.
  3. Printer pagination and Print Preview against stable printer/font metrics.
  4. Unicode fallback, IME overlay, bidi visual order, and complex shaping.
  5. ListView/TreeView pixel layout and state-image rendering.
  6. Screensaver, MCM/D3D, and DirectAnimation final composition.
  7. Paint/Calculator/pure-GDI app issues explicitly excluded above.

Any state test that already passed remains part of the regression set; the GDI gate adds visual proof rather than replacing semantic proof.

Definition of done for this plan