Original Morrowind Name consumer: static observation boundary
2026-10-28, source-only investigation. Original Morrowind.exe, 3,907,584
bytes, SHA-256 f48300e194c6917cd98d235d259ebc3ab5d7c2071e9b41d798b996a8ce4d0676.
Source baseline 5edf4c5cacf590153184bb9e347a52ffbeda86af.
Addresses below are original guest VAs, preferred image base 0x400000;
apply a verified relocation delta before using them in a future guest.
The first missing observation is the actual buffered keyboard record returned
to the original executable at 0x406926, followed through its translation
and Name edit callback. The Name control is statically connected to buffered
DirectInput, not established by the earlier WM_CHAR transport fixture. No
original guest was executed here, no historical lost character was reproduced,
and no production source repair is justified by this evidence.
Original-consumer evidence
| Original VA | Reviewed evidence and implication |
|---|---|
0x7a8f6c |
Literal MenuName_NameSpace; initializer 0x5e6580 calls 0x57fd70 and stores its 16-bit ID at 0x7c48d4. Main Name ID is stored at 0x7c4968. |
0x5e6717–0x5e672d |
Loads NameSpace ID, passes builder 0x637b50 into 0x579c40; returned child goes into EDI. 0x5e6736 passes `" |
0x637cbc–0x637cca |
The child builder registers callback 0x638050 for event 0xffff803e via 0x5738d0. Registration at 0x56ab4f names this event "key" (0x7a3d94), not WM_CHAR. |
0x4061ab |
Calls DirectInput8Create thunk 0x654260, whose IAT entry is 0x738038. Version argument is 0x800. |
0x4061f6–0x40621b |
CreateDevice uses GUID bytes 612b1d6fa0d5cf11bfc7444553540000 (keyboard); stores returned device at input-manager +0x28. Keyboard format at 0x73d700 declares 256-byte data and 256 objects. 0x40628e calls SetProperty with property ID 1 and data 0x20 (32-record buffer request). |
0x406900–0x406926 |
Reads [inputManager+0x28], invokes device vtable +0x28: GetDeviceData(this,20,data,&count,0), input count 1. No peek. Postcall 0x406926 tests HRESULT; 0x40692a tests returned count. Success with count zero or any nonzero HRESULT returns no event. |
0x406932–0x406946 |
Reads event dwData bit 0x80 and copies dwOfs out. Returns kind 1 for down, 2 for up, 0 for none/error. It does not read WM_CHAR. |
0x580700–0x58081a |
Calls that poll once at 0x580726, then translates at 0x5807bd (0x5813d0). There is no local drain loop. A nonzero translated character is stored in UI slot [ESI+0x64]=0x10, [ESI+0x68]=character, destination [ESI+0x70]. Release/modifier events can produce no character. |
0x5813d0–0x5815e8 |
Guest handles Shift scans 0x2a/0x36, stores shift byte [UI+0x84], modifier bits [UI+0x9c]; translates ordinary downs through 0x406960. English tables map scans 2e,18,20,12,2d to c,o,d,e,x; shifted table maps C to 0x43. Language selection is dynamic, not proven English at runtime. |
0x57fff0 / 0x580040–0x580056 |
In the enabled UI branch: keyboard poll/translate (0x580700), conditional alternate collection (0x580840) if no key event, then UI dispatch (0x580ec0). This proves local update order, not actual host/Worker/frame interleaving or global call frequency. |
0x580fd5–0x580fec |
Dispatch slot type 0x10 as key event 0xffff803e, forwarding slot character/aux/target to 0x574f60. 0x581399 clears a consumed slot; loop visits two slots, not the DirectInput queue. |
0x638050 |
Registered edit callback. At entry stack: root ESP+4, event +8, character/command +12, aux +16, target element +20. It obtains current text through 0x5725f0. |
0x638589–0x6385ab |
Ordinary positive character branch copies low byte to a one-byte NUL-terminated string and invokes MSVCP60 string insert via IAT 0x738154. The earlier code finds the ` |
0x6385e6 / 0x6385eb |
Calls text setter 0x5726c0, then returns to invalidation/update calls. Setter's string object is at element+0x164, data pointer object+4, length object+8. Getter may inherit parent text: distinguish actual read object from setter target. |
0x5e6895 / 0x637d30 |
Name submission gets edited text and filters ` |
Other static callers matter for attribution: 0x5adc4d also calls 0x406900;
0x580aa9 and 0x5876d8 also call UI dispatch. Their activity during Name is
unknown. Do not infer that a successful API event was consumed by this Name
invocation merely because the input manager or return address looks familiar.
Targeted next observation
The executable verifier emits a machine-readable implementation plan:
node --max-old-space-size=320 tools/probes/morrowind-name-consumer-static.js \
/READ_ONLY/installed/Morrowind.exe --self-test
This command only reads the executable. It validates its complete size/hash, preferred base and 20 original instruction/data anchors, then emits capture points and payloads. Its 22 rejecting controls flip every anchor in memory, reject a wrong full-image hash, and reject truncation. It does not implement a guest observer or claim that static checks are a production behavior test.
A future separately authorized observer should be read-only and off by default,
installed before guest initializers. Capture the Name child's actual pointer
at 0x5e672d and pin the thread, target lifetime and relocation. Observe API
entry at 0x406923 and its matching completion at 0x406926, before any
subsequent guest instruction. Before the call, the stack contains this,
stride 20, output pointer, count pointer and flags 0. After stdcall completion,
[ESP] is the returned count, and the 20-byte record begins at ESP+4.
Record HRESULT and count before interpreting bytes; zero-count/error outputs
can contain stale stack data. Record all five event fields, including sequence
and timestamp. Attribute other callers separately.
For each matched poll, capture the translated result at 0x5807c2, Shift and
modifier state, destination slot, and eventual 0x638050 callback arguments.
Require callback target to equal the captured Name child. Capture raw text,
length and cursor before setter call 0x6385e6 and immediately after it at
0x6385eb, as well as rejection to 0x6385ff. Retain the first subsequent
write that changes this same text object/backing buffer before the next
poll/render. Validate all guest pointers/lengths; truncate reads at 31 bytes
and record truncation. Never invoke a guest getter or modify input to observe it.
The first divergent boundary discriminates the causes:
- Missing down/up at successful API completion: investigate the earlier DirectInput producer, sampling/queue and device lifecycle.
- Delivered scan but missing/wrong translation or wrong callback target: inspect guest modifier, scheduling and routing state.
- Correct character at callback but no insertion: retain the edit rejection path and its width/length/cursor state.
- Correct text after setter, later shortened: identify the first later writer.
Before spending guest allowance, reject synthetic observer records with stale EAX/count, wrong invocation or wrong target. A trace missing d must identify its first absent boundary; a complete-then-overwritten trace must identify the later writer. Those observer controls remain unimplemented/unrun. Cap the future action at one ordinary Name entry and 128 observations; overflow must be retained and stop observation without replay. Runtime allocation, canonical module/runner identity and lifecycle remain coordinator gates.
What this says about the emulator
At retained source 5edf4c5c, src/09a8-handlers-directx.wat:10773
di_kbd_collect walks DIK codes in ascending order, comparing live state
against previous state and committing emitted changes up to requested count.
The keyboard GetDeviceData branch at line 11031 calls this collector.
This differs from an ordered historical edge queue: a complete press/release
between samples can disappear, and simultaneous pending changes can be ordered
by scan code. With the original executable requesting one record and keeping
guest Shift state, the distinction is now relevant to the actual Name path.
It is a source-backed candidate mechanism, not runtime proof of Codex → cox.
The prior 17-message transport checkpoint remains passing and was not replayed.
No input queue, modifier, device lifecycle, renderer, Wine/WAT, Shell or ToyVM source changed. No module/browser/game build, guest execution, Boat operation, public deployment, main push or other worker was started.
Evidence and recovery
Canonical receipt: repository task output
scratch/usage-watch/morrowind-name-consumer-cause-oct28/receipt.json.
Its evidence archive preserves owned wrapper, bounded disassemblies, data/import
identity, admissions, startup/mount/parser receipts, static verifier output,
publication and cleanup. Original oversized policy-read diagnostics remain in
the worker launch events; they were not used as disassembly evidence.
Bounded investigation helpers reused installed tools/disasm.js, lib/pe.js,
tools/simd-ops.js and objdump; helper and input hashes are in the receipt.
Two exploratory ranges started mid-instruction (0x580000, 0x5adc10);
the corrected UI prologue is 0x57fff0, and only aligned instructions cited
above support conclusions. The exploratory outputs remain retained.
Recovery requires the immutable original EXE at the hash above and the task branch named by the receipt. Obtain the small source/report changes from that branch; do not copy game assets or whole donor history. In an isolated sparse repository reuse existing donor objects read-only, with auto-GC disabled. Retain strict configured SSH for publication. Re-run the verifier only when its executable/helper/source pin changes; its current passing receipt is reusable. No runtime evidence exists to recover or repeat.