Files
vt1200/xie/README.md
T
arti d8d54bb990 feat(xie): a client-chosen XIE image is displayed on the VT1200
The XIE path now runs end to end: XieCreateImage -> XieCreatePhoto ->
XiePutTile -> XieExport(photomap -> depth-1 pixmap) -> XCopyArea(pixmap ->
window), and the client's tile appears on screen at the client's own window
position, pixel-exact.

TWO FAULTS BEHIND THE LONG DEAD END

1. Device model (harness/mame/vt1200.cpp, opt-in VT1200_ACCEL=1).  The
   accelerator's output port 0x5600200 returns the pixel stream the firmware
   pushed into the FIFO -- the firmware then stores it into the drawable itself
   (file 0x125528: MOVE *B6,*B1 ; ADDK 0x10,B1).  The FIFO read pointer was
   never reset between transfers, so consecutive exports read stale words and
   the same fill produced 759 then 516 set pixels.  Reset m_fifo_in/out on the
   data-register write; the export is now deterministic.

2. Measurement, not the device.
   - dwt/screenshot.py without --real reads 1024x1024 at a 128-byte row stride,
     but the VR319 default programs 1280x1024 at a 2048-bit (256-byte) pitch.
     That is the "shifted" picture; --real is the correct read.
   - The earlier "0 differing bytes over 128 KiB" VRAM comparison stepped peek
     by 1024 *bit* addresses, so it scanned rows 0..63 only -- never the window
     at rows 200..263.  Over the real raster the white/black diff is 576 bytes
     spanning exactly those rows.
   - New instrument: decclient does a server-side XGetImage readback of the
     pixmap and window and counts set pixels.  That separated "the server never
     drew it" from "the framebuffer view is wrong".

EVIDENCE (reproduced twice, the second time from a fresh machine)

  no export vs 0xFF : 4096 / 4096 pixels differ
  no export vs 0x00 :    0 / 4096   (the window background is black)
  0xFF      vs 0x00 : 4096 / 4096
  0xAA      vs 0x0F : 2048 / 4096   (the two stripe phases)

Tile byte 0xAA reproduces as 1-pixel vertical stripes, 0x0F as 4-pixel, with
no shear -- so the placement is right and the pixels are the client's.

Contents: xie/ holds DEC XIE 2.3 headers, 84 man pages and complete
client+server source; a libXie.a and decclient built from DEC's own code; the
XIE probes; and evidence/ with every measurement, including explicit
retractions of the earlier wrong readings (VRAM_IDENTICAL_FOR_BOTH_FILLS and
friends).  xie/evidence/XIE_IMAGE_DISPLAYED.md is the report; frames are in
xie/evidence/png/.

The accelerator model stays opt-in: it answers the status poll and relays the
pixel stream, but performs no compositing, so it is a stand-in and the verified
default configuration is unchanged (a no-env run still parks in the level-0
diagnostics menu at PC=3FDCE400).
2026-11-01 23:22:25 +02:00

55 KiB
Raw Blame History

XIE sources: xieperf, XIElib and the sample server extension

STATUS (updated): a client-chosen XIE image IS displayed on the VT1200. The working path is XieCreateImage -> XieCreatePhoto -> XiePutTile -> XieExport(photomap -> depth-1 Pixmap) -> XCopyArea(pixmap -> window), run with VT1200_ACCEL=1 and the compression poke in harness/mame/xie_fix.lua. Two faults were removed: the accelerator's output-FIFO read pointer was never reset between transfers, and dwt/screenshot.py needs --real (1280 wide, 256-byte stride) for the VR319 default. See evidence/XIE_IMAGE_DISPLAYED.md and evidence/png/xie_window_tiles.png. Everything below this banner that says "NO client-chosen XIE image displayed" is historical and is superseded; the corrections are listed in that report.

Vendored copy of the XIE (X Image Extension) sources from X11R6.1 (X Consortium, March 1996). The point of interest is xieperf, the XIE test/benchmark client, which is the natural first client to point at the VT1200's DECwindows server because that server advertises a Xie extension:

$ xdpyinfo -display 192.168.99.2:0
...
number of extensions:    2
    ServerManagementExtension
    Xie
...
vendor string:    DECWINDOWS DigitalEquipmentCorp.
version number:    11.0
...
depth of root window:    1 plane

What is here

path contents
xc/programs/xieperf/ xieperf client: 35 .c files, xieperf.h, Imakefile, xieperf.man, and the images/ test images (image.001–image.014)
xc/lib/XIE/ XIElib, the client library xieperf links against
xc/include/extensions/XIE*.h XIE.h, XIEproto.h, XIEprotost.h — wire protocol and client API headers
xc/programs/Xserver/XIE/ the X11R6 sample server-side XIE implementation (reference only; not needed to build xieperf)
xc/doc/hardcopy/XIE/ XIE protocol spec, XIElib spec and sample-implementation architecture, as plain text

280 files, about 8.3 MB. PostScript copies of the documents were dropped; the .txt renderings are kept.

Provenance

Downloaded 2026-11-01 from the official X.Org release archive:

Checksums of the original tarballs:

d138b68392d4ca49939fd0da46180763a23e82838fb30cb964ed2c5bc6d6c8d6  xc-1.tar.gz
cec75d6ae72d437783c72c2892d637a8a2acfd0f2088358ed024064a1a687976  xc-2.tar.gz
a980821b36607773eba8735ad5608e03c503fe139656f4e3beb64c1b5049fab9  xc-3.tar.gz

xc-2.tar.gz holds the same specs as nroff sources under xc/doc/specs/XIE/; it was not needed for this copy.

Other places the same tree is available:

XIE was removed from the reference X11 distribution in X11R6.7 (2004), so there is no current distro package for xieperf.

Licence

X Consortium / MIT-style terms for the X11 code, plus the AGE Logic notice on the XIE sources. Both are reproduced at the top of every source file and in xieperf.man; the AGE Logic terms permit use, modification and sale provided the notice is preserved.

Using it against the VT1200 — status

It builds and connects, and it does identify the server, but it cannot yet run the test suite: the VT1200 implements XIE protocol 2.3, while xieperf speaks 5.0. Verified against 192.168.99.2:0:

xieperf - XIE performance program, version 1.0

XIE V2.3
FULL server
Screen visual class is StaticGray
Colormap has 2 entries
Effective depth is 1-bit
...
Class name: ColorAlloc
X Error of failed request:  BadLength
  Major opcode of failed request:  129 (Xie)
  Minor opcode of failed request:  0 ()

The request that kills it is the client's 5.0 QueryTechniques (minor 2). In 2.3 that slot is not QueryTechniques: the ROM dispatch table says slot 2 accepts exactly one word and returns success without a reply, while the 5.0 request is two words and gets BadLength. So xieperf hangs at one word and dies at two.

The ROM analysis (GHIDRA_XIE.md) found 46 request slots, 25 implemented. Only slot 1 can be matched to its 5.0 counterpart; the numbering is otherwise not 5.0's. No slot implements QueryTechniques or any technique-capability reply, so -showtechs cannot work against this server.

Raw X11 probes (no XIElib) plus the ROM table give this map:

minor 5.0 request the client thinks it is what 2.3 slot N actually does
1 QueryImageExtension same request (2 words); replies 2.3, class 0x20, 8 levels
2 QueryTechniques 1 word, success with no reply; not a query
6 QueryColorList 1 word (bare header) → reply, length 0 (no argument)
11 QueryPhotomap 3 words (5.0: 2); id at byte 4 → BadIDChoice if null
23 GetClientData not implemented → BadRequest
24 QueryPhotoflo 4 words (5.0: 3); id at byte 4 → BadIDChoice if null

BadLength always arrives with minor_op = 0 because the handlers return 16 to the dispatcher and the generic error path has no minor to report; errors XIE sends itself do carry the real minor (minor 23 reports BadRequest with minor_op 23). So the 0 in the error is a server-side artefact, not evidence about which request failed.

The full evidence is in evidence/ (xie_wire_probe.txt, xieperf_showtechs_vt1200.txt, xieprobe_vt1200.txt) and ghidra-evidence/ (dispatch_table.txt, listings.txt, length_sweep_wire.txt).

So: the extension is real and answers XIE requests; xieperf 5.0 is the wrong client for it. A minimal 2.3 session client now exists:

./xiectl23.py        # against 192.168.99.2:0
extension present=1 opcode=129 first_event=64 first_error=0
init   (1,2w): REPLY len=8  010102000800000002000300...
qdefs  (3,2w): REPLY len=0  0141030000...
qevents(6,1w): REPLY len=0  010004000000000002000000 01000000 01000000 01000000
term   (2,1w): TIMEOUT                 # Success, no reply

Order matters: QUERY_EVENTS returns 2,1,1,1 only after INIT_SESSION; sent alone it returns zeroes. In the INIT reply only major 2 / minor 3 are stable across connections; the tail words vary, so do not parse them as a fixed capability record.

That is the session half. Live probing has since established the resource rules: the per-connection id base/mask, the reserved bit in new ids, slot 8 op 2 as a working create, and the exact reason slot 21 refuses to export (BadMatch from resource flag +0x28). The exportable object lives in the photoflo/pipeline layer, not in slot 8. See evidence/xie_create_export_probe.txt, GHIDRA_XIE_IMAGE_PATH.md and GHIDRA_XIE_PIPELINE.md (when present).

Is there an XIE client inside the ROM?

No. The ROM contains the XIE server only. A MAME trace with a breakpoint on XieProc (0x3F525CC0) saw zero hits in three runs:

run configuration XieProc hits
plain boot → level-0 diagnostics 1200 frames 0
VT1200_SKIP_LEVEL1=1 → OS/Terminal Manager 4500 frames 0
forced jump into the Scalution test (0x35EA8) 900 frames 0
control: same tracer on the menu idle PC 60 frames 4,634,965

The control run matters: MAME discards debugger printf output unless it is started with -debuglog, which is why the first zeroes were not yet trustworthy. With -debuglog the control fires, so the zeroes are real.

The “5 - Scalution Test” is a hardware diagnostic: its body writes the image-option/scaler registers (0xCA2B0, 0xCA330, 0xCA3A0, 0x5600020, 0x5600030, 0x5600100) directly. It never calls the server or builds an XIE request. The ROM’s only Xie strings are the server registration messages.

So there is no working 2.3 client sequence to replay. The client in this directory is ours, and the PUT_TILE band gate still has to be settled from the server model plus live probes. Report: MAME_XIE_TRACE.md; reusable tracer: harness/mame/xie_trace.lua.

Clients and tools in this directory

file what it is status
xiectl23.py minimal XIE 2.3 session client: INIT_SESSION → QUERY_OP_DEFAULTS → QUERY_EVENTS → TERM_SESSION verified against the VT1200
probe23.py live raw-X11 probe with a persistent connection: session, defaults, raw slots, create/sweep used for all wire evidence
xshow.c / xshow 1-bit image viewer using core X11 PutImage; reads the window back and compares on-pixel counts verified on the VT1200 (MATCH), not XIE

xshow is the display half and an honest fallback, not a pure XIE image client. The XIE path itself is now much further than the session: the create request with a non-zero level word at byte 28 (2 = 1 bpp) produces an exportable resource, and slot 21 exports it into a real X window + GC with no error.

Slot 20 arms the resource's bands. The request that succeeds is 35 words with id@4, byte8 = 64, byte12 = 2, byte14 = 1, byte16 = 1, byte13 = 1 (mode). After that, PUT_TILE no longer returns BadAccess; it reaches the class check:

  • class-2 resource (create op 2) → BadMatch: PUT_TILE passes class 3.
  • class-3 resource (create op 1) → BadIDChoice.

So the target is a class-3 resource with byte 40 == 0. WITHDRAWN — the byte-40 rule was a misreading. Measured instead:

  • create op 1 and create op 2 call the same routine (file 0x678DA); the only op-dependent value it passes is (op == 2) ? 1 : 0, and op is also the level loop count (DSJ at 0x67A24).
  • create op 1 → object class 3 but id@0 == 0: the ID is stored nowhere in the object, so the table lookup can never find it → BadIDChoice.
  • create op 2 → object class 2 with id@0 set → addressable.
  • Only payload word 1 is accepted as the ID (any other position → create BadIDChoice).
  • Byte 40 is a copy-out field: the helper (file 0x66FA0) copies *obj(0x140) out when the caller passes a non-NULL out-param, and only the NULL-out-param variant tests it — raising 0xE, not 0x8. So patching byte 40 could never have fixed PUT_TILE, which is what was measured.

So the real missing piece is the request that both selects class 3 and stores an ID. Evidence: evidence/class3_id_not_stored.txt.

The create opcode space is exhausted

Every accepted create opcode has now been tested at its own required length:

op=1  12 words  create OK   allocates   query BadIDChoice  put BadIDChoice
op=2  12 words  create OK   allocates   query REPLY        put BadMatch
op=3            BadIDChoice
op=4   6 words  "OK"        allocates NOTHING, ID never addressable
op=5   7 words  "OK"        allocates NOTHING, ID never addressable
op=6  >=6 words "OK"        existing resource left intact and queryable

op 4 and op 5 are not creates — both begin by looking up an existing resource (op 4 looks up payload word 2 through 0x7EA6FA0; op 6 looks up payload word 1 through 0x7F174A4). Only op 2 yields an addressable resource, and its class is 2. So no create opcode produces a class-3 resource registered by ID. Evidence: evidence/create_opcode_sweep.txt.

Slot 11 is not the class-3 bind

slot 11 needs exactly 3 words (2 ids) and performs two typed lookups (call #1 pushes the literal 0xffff, call #2 pushes 0). With two valid, queryable class-2 resources:

A=0040C000 class=2 b40=0     B=0040D000 class=2 b40=0
slot11 [A,B] -> BadIDChoice (14)
slot11 [B,A] -> BadIDChoice (14)
after: both unchanged, both still queryable, put -> BadAccess (10)

The operand types are fixed inside the handler, so slot 11 cannot be steered to a class-3 bind from the request. Evidence: evidence/slot11_bind_and_second_allocator.txt.

The element is created by EXPORT, not filled by PUT_TILE

element = *( *(res+44) + 0xd8 ) is 0 after create, after arm1, and after PUT_TILE; it only becomes non-zero inside EXPORT (0x1B234: EX_BAND a5=003DC4B0 elem@d8=003FDA10). No write to element+0x28 was ever observed. So PUT_TILE does not populate the element — EXPORT builds it — and the source image pointer it copies stays 0. Evidence: evidence/element_pointer_timing.txt.

Export resolves the resource; only src_base is empty

Values written by the export copy block (0x1B296), named against the firmware's own dump strings:

rp+0x5a8 = 0x0040C000   src_res_id   -> our resource, correctly resolved
rp+0x5d0 = 64           src_width
rp+0x5f0 = 64           src_height
rp+0x610 = 0            src_base     <-- the pixel buffer: MISSING
rp+0x738/0x758 = 64/64  dst_img PWIDTH/PHEIGHT

So EXPORT is not failing to find the resource — it has the id and the 64×64 size. The single empty field is src_base, loaded at 0x1B238 (MOVE *A5(0x28),*A11(0x610)), which no write watchpoint ever saw written. The missing datum is a pixel buffer for the source image, produced only by the import. Evidence: evidence/export_renderparams_map.txt.

slot 47 is a byte-swapped dispatch trampoline — and is unreachable

file 0x697B4 swaps the bytes of request words at bit offsets 0x10/0x18, 0x20/0x28, 0x30/0x38, then re-dispatches through the same table at 0xD3F90 (SLL 5 ; ADDI 0xd3f90 ; MOVE *A11,A11 ; CALL A11).

Tested and closed: with breakpoints on the trampoline body (0x3F54BFF0 table read, 0x3F54C000 CALL — count 36→38, no command errors), minor 47 returned BadRequest for plain and byte-swapped argument words, and the trampoline never executed (0 TRAMP hits). So there is no client-reachable byte-swapped request family. It is also self-referential: the swaps cover offsets 2–7 only, never the minor byte at offset 1, so a client request for minor 47 could only re-enter it.

The default handler 0x64C3C (shared by every unimplemented slot and by slot 0) is the BadRequest responder — which is why probing unimplemented slots returned ERR1. Evidence: evidence/trampoline_unreachable.txt.

src_base sweep across every remaining slot — none writes it

Each remaining implemented slot was exercised at its own required length with [resource_id, 0, 0…], in a full create→arm1→<slot>→PUT_TILE→arm2→EXPORT sequence, reading rp+0x610 from the export breakpoint and diffing the window against its pre-export content:

slot 18,40,44,33,14,15,9,13,24 :  src_base = 00000000   window changed = False

Every export still resolves the source id and the 64×64 geometry (5a8=<id> 5d0=40 5f0=40), and 610=0 in every case. Coverage is now complete: every implemented slot (1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 18 19 20 21 22 24 33 40 43 44, plus the BadRequest default and the unreachable trampoline) has been exercised against a created resource.

This is not a proof of impossibility: only one argument shape per slot was tried. Evidence: evidence/src_base_sweep_all_slots.txt.

Multi-operand shapes fail at the same typed-lookup wall

The bind-family handlers (33 40 44 18) and 43 were then given two and three live ids. Two findings:

  • The third operand is an id, not a flag: slot33 [A,B,t] for t=1..9 returns BadIDChoice with bad = t (and t=0 → BadValue).
  • With three valid ids the lookup still fails, because it is typed:
slot33 [A,B,A] / [A,B,B] / [A,B,C] / [A,A,A] -> BadIDChoice, bad = 3rd id
slot40 [A,B,A] / [A,B,B]                     -> BadValue,    bad = A
slot44 [A,B,A,0] / [A,B,B,0]                 -> BadValue,    bad = A
slot18 [A,B,0]                               -> BadMatch
slot43 [A,B,0...]                            -> BadValue

Every multi-operand request fails at a typed lookup of one of its own operands: the operand exists and is queryable, but lacks the class the handler requires. That is the same wall as PUT_TILE, reached from four more directions. src_base stayed 0 and no window changed. Evidence: evidence/multi_operand_shapes.txt.

The single wall, restated precisely

Every client-reachable path — single- and multi-operand, all implemented slots — fails for one reason: the server's resource model requires a resource class that its create path will not issue with a stored id (create op 1 builds a class-3 object but leaves id@0 = 0). Until that class can be created addressably, no request reaches an import that would set src_base.

Why that class cannot be created addressably — CORRECTED

Superseded. The "mutually exclusive" argument below is wrong: op=1 does reach the registering body. Measured with breakpoints on 0x67B04/0x67B34, create op=1 registers id 0040C000 → object 0036A950, which is a class-1 photoflo (id@0 set, w=3259088 h=1, query → BadAccess). create op=2 registers a class-2 resource (003FD7D0, w=64 h=64, query → REPLY). The class-3 object seen earlier (class 3, id@0 = 0) is an intermediate that is never the registered object. So the accurate statement is: the id is registered to a class-1 photoflo or a class-2 resource; no create opcode registers a class-3 object. Evidence: evidence/registration_hits_corrected.txt.

Registration audit: only class 1 and class 2 ever get an id

A full audit with breakpoints on 0x67B04 (ID store) and 0x67B34 (register):

arm1 / put / arm2 / slot43 / export / arm1(photoflo) / put(photoflo) -> 0 hits
create op=2  id=0040C000 -> REGIDSTORE obj=003FDA10  (class 2, query REPLY)
create op=1  id=0040D000 -> REGIDSTORE obj=00385890  (class 1, query BadAccess)

Registration happens only inside create, and only those two object kinds ever receive an id. The class-3 object built by the allocator (file 0x6CA82) never appears in a REGIDSTORE.

Walking the photoflo's +0x160 chain the way export does (0x1B230 MOVE *A5(0x160),A5):

lvl0 00385890 id@0=0040D000 class=1   p44=00386010   <- resource-like
lvl1 00386010 id@0=003859D0 class=80  p44=00386290   <- first word is a pointer
lvl2 00386290 id@0=00386150 class=144 p44=003C7150
lvl3 003C7150 id@0=003863D0 class=144 p44=00000000   chain ends

Only lvl0 has a resource layout; from lvl1 the first word is a pointer and the "class" byte is 0x50/0x90, so those are internal blocks, not resource structs, and carry no client id. So there is no client-visible id whose registered object is class 3 — the two ids a client can obtain are registered to class 2 and class 1. Evidence: evidence/photoflo_chain_no_id.txt.

The armF state gets furthest: BadAccess instead of BadMatch

Tracing the one configuration never followed before (I = image, F = photoflo):

create I 0x40C000 -> REGIDSTORE id=0040C000 obj=003FDC50 ; REGISTER
create F 0x40D000 -> REGIDSTORE id=0040D000 obj=00388090 ; REGISTER

arm F then PUT_TILE I  ->  S19LOOK a8=0000000A   (BadAccess,  10)
arm I then PUT_TILE I  ->  S19LOOK a8=00000008   (BadMatch,    8)
PUT_TILE F (unarmed)   ->  S19LOOK a8=0000000E   (BadIDChoice, 14)

Arming the photoflo genuinely moves the typed lookup past the stage that yields BadMatch and stops it at a BadAccess check — a real state difference and the furthest any configuration has reached. No configuration reached the handoff (0x6B640) or wrote src_base; the registration sites get 0 hits outside create.

Caveat, recorded because it invalidates a column I have been quoting: the LOOK breakpoint at 0x6BA38 prints cls=01 for both the armF and armI cases, and earlier printed an incrementing 01,02,03 across calls. So A7 there is the export element, not the resource struct — its cls/b40 columns are not resource fields. Only S19LOOK's return code (a8 at 0x6A7B4) is trustworthy. Evidence: evidence/armF_lookup_codes.txt.

Three more assumptions tested and falsified

Chasing the BadAccess, three structural guesses were each disproved within one step — recorded so nobody repeats them (evidence/three_wrong_assumptions.txt):

  1. 0x7F638C2 is not the allocator — file 0x1238C2 is a printf-style logger (format descriptors 0x3fb1cea0 etc.). The real allocation is 0x7EACA82 → file 0x6CA82.
  2. 0x7F174A4 is not the XIE resource lookup — file 0xD74A4 indexes a table at 0x21FFE0 by N = (id & 0xFFF00000) >> 12, bounds-checked against a count of 16 at 0x225870. That is the core-X client index; for our ids N = 1024, out of range, bucket reads 0.
  3. @0x1AD980/@0x1AD990/@0x1AD9A0 are not XIE list heads — they read 0x02000002, 0x04000200, 0x00000400, and following them as pointers reads zeros.

The direct observations stand (S19LOOK a8 = 0x08 / 0x0A / 0x0E at 0x6A7B4), but every explanation built on those three functions is now void.

The slot-19 lookup is file 0x6B966 — and it does not produce the 0x8

rom:0006a7ae CALLA 0x07eab966 → file 0x6B966 (not the 0x66FA0 helper). Its flow:

6B990 CALLA 0x07F174A4          ; returns an object pointer
6B99A JRNE 0x6BA8C             ; null -> MOVK 0xe (BadIDChoice)
6BA8C A8 = object->byte4       ; class byte
6BA90..6BA9C  class 1 -> 0x6B9C4 ; class 2 or 3 -> 0x6BA1E ; else 0xe
6BA1E arg3==NULL ? require object->byte40 == 0 (else 0xe) : copy byte40 out
6B9C4 (class 1) requires arg3 != NULL, then a two-step byte8->byte8 /
      byte12->byte8 deref

It raises only 0xE and 0xA — there is no 0x8 route before 0x6BBC0.

Measurements that contradict a simple story, recorded as-is:

  • sweeping request payload word 5 (the arg6 the code branches on) over 0,1,2,0x100,0x10000,0x80000000,0xFFFFFFFF,0xFFFFFFFE gives BadMatch every time — arg6 has no observable effect;
  • the 0xE-build site (0x6BA86) did fire during a put whose reply was ERR8;
  • the 0xA site (0x6BAC6) and an in-function MOVK 0x8 site (0x6BB4C) did not fire during that put;
  • the shared reply helper 0x7EFFA5E did not fire at all.

So the ERR8 the client receives is not produced by any site we can see, and site-based attribution is unreliable. Evidence: evidence/lookup_is_6B966.txt.

Bounded next step: disassemble the remainder of 0x6B966 (0x6BBC0..0x6BBE4) and the 0x6BAEA path, logging A8 at every return, to find the 0x8's true origin. Until that is known, BadMatch cannot be interpreted.

Both properties the import needs are decided in the same routine (file 0x678DA):

679A2 SUBK 0x2,A11 ; MOVK 0x1,A11
679A6 JREQ 0x679AA          ; op==2 -> flag 1
679A8 SUB A11,A11           ; else  -> flag 0
...
6CAF6 JREQ 0x6CAFC
6CAF8 MOVK 0x2,A11          ; flag!=0 -> class 2
6CAFC MOVK 0x3,A11          ; flag==0 -> class 3      <-- 6CAFE stores it
...
67A20 MOVE *A9(0x20),A11    ; A11 = op
67A24 DSJ A11,0x67AFC       ; op-1 != 0 -> jump to the REGISTERING body
67A28 ...                   ; op-1 == 0 -> photoflo path
67B04 MOVE *A9(0x40),*A11(0x0)   ; ID -> obj+0
67B34 CALLA 0x07F16CA2           ; register in the resource table
  • op = 1 → flag 0 → class 3, but DSJ falls through to the photoflo path (0x7F63A30, 20 words, error string "error allocating photoflo"), so the ID is never stored and never registered.
  • op = 2 → flag 1 → class 2, and DSJ jumps so the ID is stored and the resource registered.

PUT_TILE requires class 3 (slot 19 pushes MOVK 0x3 before the typed lookup), and the only class-3 producer never registers. So the import is unreachable — not because a request shape is missing, but because the two properties the lookup requires cannot coexist in the implementation.

op=4 was also ruled out: it increments a counter on the target (obj word at byte 6: 1→2→3 after two calls, no new allocation) and PUT_TILE stays BadMatch at every count. Evidence: evidence/class3_registration_impossible.txt.

MAME trace: the limit we reached

A single MAME instance on the host tap (-networkprovider taptun, -cfg_directory harness/mame/cfg-net) makes the emulated terminal available at 192.168.99.2, and harness/mame/xie_resource.lua traces XieProc requests and the resource lookup. With -debuglog, every request appears as XIEHIT buf=... 0181 0002 ..., and the lookup check prints LOOK a7=... id=... class=... b36=... b40=....

Results:

  • PUT_TILE can be forced through the class check with a debugger branch skip (0x6BAF6 → 0x6BBCE); it then returns success — a false success, see below.
  • The exported window is still independent of the tile data: solid-white and solid-black tiles produce the same 64×64 pattern. That pattern is the XIE export default/dither, not the supplied pixels. WITHDRAWN.
  • Therefore the missing piece is the band/execute format (the photoflo element/bind layer), not just the class check. The class skip only lets the request through; the data never reaches the export sink.

Correction: export has never painted anything

The 8888…2222… pattern is the window's own default background, not XIE output. Measured on one instance, one connection, 64×64 window:

BASELINE (window created+mapped+GC, NO export)  nz=512 first=88888888888888882222222222222222
AFTER    (create→arm1→PUT_TILE→arm2→EXPORT)     nz=512 first=88888888888888882222222222222222
identical to baseline: True

So every white-vs-black comparison was vacuous. The measured reason export draws nothing: the chain is intact but has no source image —

EXPORTENTRY
EXPORTFLAG f=01                       ; global @0xA6220 non-zero, render path taken
EX_BAND a5=003DC4B0 elem@d8=003FDA10  ; photoflo→band→element IS present
EX_ELEM a5=003FDA10 f28=00000000      ; element+0x28 == 0  → no imported image

The driver copies element+0x28 into the render params (0x1B238 MOVE *A5(0x28),*A11(0x610)), and the per-element render loop (0x1BC38..0x1BF30, element types 1–5 = crop/scale/rotate/mirror/export) is never reached. Because the import never runs (PUT_TILE → BadMatch), nothing fills element+0x28. Evidence: evidence/export_paints_nothing.txt, evidence/export_chain_trace.log.

The correct acceptance test is therefore "export output differs from a no-export baseline", not "export matches a known pattern".

A band dump at slot 20's comparison (0x6AB88) shows the arming state f1=01 f2=02 f4=00010000 f8=0 f12=0 f16=0. The second execute produces no BAND line at all: it fails in the lookup/state path before the band comparison, so no choice of the 35-word comparison fields can reach it.

The 0x6BA38 pointer was then tested as the resource: a second connection's before/after peek at the same address (0x003FDA10) returned the terminal message string "Connection request from IP host 192.168.99.1", identical before and after PUT_TILE. So that breakpoint is an unrelated lookup, not the XIE resource; the requested resource diff cannot be obtained with it. GHIDRA_XIE_IMPORT.md's mapping of that path is therefore wrong. Evidence: evidence/mame_look_not_resource.txt.

The real resource was then located by scanning the XIE table entry for the client id (id followed by handler pointer 0x3F566ED0; resource pointer at +12). At 0x003FDC50, the struct is id | class=02 | width=64 | height=64 | band-ptr@36 | ptr@44. Its band record is at resource+36 (0x008FE730). A full before/after diff around PUT_TILE shows:

  • resource struct: byte-identical
  • band record: byte-identical
  • XIE globals 0x1AD800..0x1ADB00: no changed bytes
  • second slot-20 execute: still BadMatch

So the post-put gate is not in the resource, the band record, or that global block. Evidence: evidence/mame_state_diff.txt.

Write watchpoints during PUT_TILE then identified the gate:

WPHIT res  addr=003FDC80 data=00000000   ; resource byte 48 = 0
WPHIT band addr=008F6750 data=00000001   ; band byte 12 = 1

Slot 20 ANDs band+0x60 (byte 12) with request byte 16 and requires zero, so after the put the second execute must use v16 = 0:

arm1 v16=1 -> OK
put          -> OK
arm2 v16=0 -> OK
arm2 v16=2/3/4/0x10 -> BadMatch

That solves the arm-2 gate. Export is still independent of the pixels: solid-white and solid-black both give nz=512 first=88888888…2222…. The band sink pointer band+0x30 is zero; poking it to the resource’s +44 pointer did not change that.

Slot 7 {op=1, type=0, id} was then tested as the bind: with it PUT_TILE returns BadIDChoice, and the export pattern is unchanged. So slot 7 is not the sink bind either. Evidence: evidence/mame_bind_slot7.txt.

Static decode of the payload handoff 0x7EAB640 (file 0x6B640) shows it is the import-element executor: it selects element type 1/2/3 from client+8, takes the band subobject at band+0xd8, calls 0x7E80008, and clears the transient payload pointer at band+0x28. band+0x30 is not used by the handoff at all. So the band sink assumption was wrong; the missing piece is the three-element pipeline definition and its export element, not the handoff target. Evidence: evidence/payload_handoff_6b640.txt.

A bit/byte correction matters: *A9(0xd8) is byte 27. The create writes the band record pointer at byte 27 of the +44 array (u32@27 = 0x003FDA10); the +36 array has zero at that byte but slot 20 does not use it for the handoff. So the handoff itself is not a no-op; the element array used by slot 20 is the one that lacks the record.

Attempting a three-element pipeline (create with band count 3 and levels [2,2,2]) makes arm 1 fail BadMatch; the one-band path works but export is still data-independent.

Populating the +36 element record by rdebug poke (r36+27 ← 0x003FDA10, the valid band record from the +44 array) also leaves export unchanged: solid-white and solid-black both give nz=512 first=88888888…2222…. So the export path is not driven by that element array either. Evidence: evidence/element_poke_no_effect.txt, evidence/pipeline_3_elements.txt.

Calling slot 21 before PUT_TILE also gives the same pattern. Slot 21 allocates a 0xC8-byte export context, calls 0x7E5B152, then frees the context at 0x630B0; the export process block reads the band’s transient payload pointer at band+0x28 (byte 5), which slot 19 clears after the handoff. No ordering of the current requests captures the tile. Evidence: evidence/export_before_put.txt.

The device-dependent dispatcher 0x7E80008 (file 0x40008) was instrumented at its CALL A11 (0x40086): no DDCALL occurred during PUT_TILE. The tile data does not reach a DD element worker at all; the dispatcher’s early type checks reject before the worker call, or the handoff does not reach it.

Instrumenting the whole path then showed the stronger result: with XIEHIT confirming that minor 19 reaches XieProc, none of the slot-19 entry (0x6A76E), the handoff (0x6B640), or the DD dispatcher breakpoints fire. The “PUT_TILE OK” is the debugger branch-skip taking an early return, not an import. The earlier put OK results are therefore not evidence of import, and the minor-19 handler mapping itself needs re-verification. Evidence: evidence/class_skip_false_success.txt, evidence/no_dd_worker.txt.

The live dispatch table then confirmed entry 19 is correct (0x3F553B70 → file 0x6A76E), and with the branch skip removed the real trace is:

XIEHIT ... 1381 0088 ...     ; minor 19 reaches XieProc
S19ENTRY                     ; 0x6A76E reached
S19LOOK a8=00000008          ; lookup returns BadMatch
put -> ERR8 bad=id minor=19
no S19HANDOFF                ; 0x7EAB640 never called

So the real path is blocked at the lookup: it needs a class-3 resource that is registered by ID. Evidence: evidence/dispatch_entry19.txt.

Superseded: the "byte 40 == 0" requirement stated here and below was a misreading. Byte 40 is a copy-out field, and the actual reason class 3 is unreachable is that create op 1 never stores the ID at obj+0. See evidence/class3_id_not_stored.txt and the correction near the top of this file. The paragraphs that follow record what was tried under the wrong assumption; they are kept for history, not as conclusions.

The bind/copy helper 0x672A4 explains why no class-3 resource qualifies: for class 2 or 3 it executes resource+0x140 = source pointer (0x673F4). Slot 11 is a two-resource bind (lookup id@4, lookup id@8, call 0x672A4), so any class-2/3 resource created or bound through this helper has a non-zero byte 40. No request in the recovered set produces class-3 with byte 40 clear; slot 43 calls the same helper, and its type descriptors/inputs remain the only untried route. Evidence: evidence/bind_672a4.txt.

Slot 43 with the source id duplicated as its destination field ([A, A, 0…]) partially changes the resource — the lookup error moves from BadMatch to BadAccess once — but the state is unstable: with slot-20 arming it returns to BadMatch, and the real PUT_TILE never passes. No class-3 resource with byte 40 = 0 and a usable band is produced. Evidence: evidence/slot43_self.txt.

Instrumenting slot 43 directly (0x65BD0 object, 0x65C46 ID store) for [A, 0] gives:

S43OBJ a8=00385D90        ; object allocation hit
S43ID                     ; ID store NEVER hit
slot43 -> BadValue
query 0 / put 0 -> BadIDChoice

The hit object is a temporary process block; poking its first word to a valid ID B still leaves B unaddressable (BadIDChoice). Slot 43 fails before the ID store, so it does not create an addressable class-3 resource. Evidence: evidence/slot43_instrumentation.txt.

A later attempt to locate the live resource by a field-filtered scan also failed: the 0x60000..0x180000 hits were class-0 copies, and 0x3E0000..0x400000 contained no matching struct. The resource pointer is not stable across runs, so the class-byte patch could not be applied to the real resource. Evidence: evidence/resource_scan_fail.txt.

A deterministic capture at 0x6CC0E (RESPTR a7=…) finally gave the real pointer. For an op-1 (class-3) resource, byte 40 was 0x003C73D0; patching it to 0 still made the real PUT_TILE return BadIDChoice:

resource 0x003FDC50 class 3, byte40 0x3C73D0 -> 0
put -> ERR14   S19LOOK a8=0000000E (BadIDChoice)   no S19HANDOFF

So a class-3 resource is not in the lookup table slot 19 searches, even with byte 40 forced to 0. The class-3 namespace is separate; patching the struct does not register it for PUT_TILE. Evidence: evidence/class3_b40_patch.txt.

The remaining gap is the destination/sink for the tile data and the export draw path. Evidence: evidence/mame_put_gate.txt, evidence/mame_put_watchpoints.log.

That leaves the same conclusion as before, now with a concrete invalidated assumption: the post-PUT_TILE state is not reachable through the recovered addresses, and the 2.3 element/state format remains unknown.

Evidence: evidence/xie_export_window_vt1200.png, evidence/xie_export_crop.png, evidence/mame_xie_resource.log, evidence/mame_band_arm2.log, evidence/mame_look_dump.log.

This is the honest ceiling of the current work: the server is understood well enough to create, arm, put and export, but the undocumented pipeline element format is still required to make the exported image be the client's pixels. A working DEC XIE 2.3 client library or protocol document is the missing external input; nothing in the ROM supplies it (MAME_XIE_TRACE.md shows the built-in image test drives hardware directly and never calls XIE).

./xiectl23.py                       # XIE 2.3 session
./probe23.py defaults               # set/query op defaults
./probe23.py cs7 0xC0AAAA 0xFFFF    # create probe + resource sweep
gcc -O2 -o xshow xshow.c -lX11
./xshow -display 192.168.99.2:0 -pattern checker -seconds 30

Other server properties that will matter once a 2.3 image client exists:

  • Depth 1 / StaticGray. The VT1200 root is 1 bit deep, so pipelines that end in ExportDrawable must match that visual (ConvertToIndex/Dither/ Point); ExportPhotomap tests display nothing.
  • Image files. Run from this directory or pass -images xc/programs/xieperf/images.

DEC documentation

docs/dec/ holds DEC's own manuals, downloaded rather than guessed at:

  • DECimage-1200-install-use-...pdf — the closest DEC document to this server. Appendix C says the terminal "uses a subset of the XIE protocol", lists the supported functions (session/resource/transport/geometric/process control) and the pipeline restrictions.
  • VT1200-user-guide-...pdf and VT1200-release-notes-V2.0-...pdf — no XIE content, but they establish what the base terminal documents.
  • DTJ-1991-v03-04-...pdf — DEC Technical Journal article on the bitonal image accelerator, citing X Image Extension Concepts, Version 2.4.
  • VXT2000-user-information-XIE3.0.pdf — the later DEC terminal that explicitly documents XIE protocol 3.0.
  • DECwindows-Motif-OpenVMS-XIE-support.pdf — DECwindows Motif shipped XIE 3.0 (XIE$SHRLIB.EXE + headers), not 5.0 and not the VT1200's 2.3.

See docs/dec/README.md for quotes, part numbers, provenance, and the software-media checks. DEC did not publish the 2.3 wire encoding in these manuals, and the surviving Ultrix UWS tapes checked carry no XIE development kit. So request numbers and lengths still come from the live probes and the ROM analysis.

The closest surviving source is already in this tree: xc/programs/Xserver/XIE/ is DEC/AGE's server-side implementation (protocol 5.0, session.c by Dean Verheiden, AGE Logic, 1993), the direct descendant of the ROM's dispatcher. It is the structural map the Ghidra work is using.

Building

It builds on a modern toolchain, without imake, using the host's X11 headers and libraries:

./build.sh
# built: .../xie/build/xieperf/xieperf

Requires gcc, libX11-dev, libXmu-dev (and ar). build.sh stages the XIE headers as <X11/extensions/...>, compiles xc/lib/XIE into build/libXIE.a, then compiles and links the 35 xieperf sources against it with -lXmu -lX11 -lm.

Flags that matter: -std=gnu89 keeps the K&R-era source legal on GCC 16, -fcommon preserves its tentative definitions, and -w hides the expected prototype warnings. This was verified with GCC 16.2.1.

The original imake build (Imakefile: $(XIELIB) $(XMULIB) $(XLIB), MathLibrary) still works in an X11R6.1 tree, but needs the whole imake configuration.

Local fixes for DECwindows and modern libX11

Two changes were made to the vendored xc/lib/XIE/init.c (both marked with comments in the source):

  1. Display-lock deadlock. XieInitialize holds the display lock and then calls XInitExtension, a public Xlib function that takes the same lock. Old single-threaded Xlib made LockDisplay a no-op unless XInitThreads had been called, so the bug was invisible; on a modern threaded libX11 it self-deadlocks in pthread_mutex_lock inside XQueryExtension. The lock is now dropped around XInitExtension and re-taken afterwards.

  2. Extension name. XIE's canonical name is "XIE", but DECwindows registers it as "Xie", and extension names are case-sensitive. On the VT1200:

    XQueryExtension("XIE") = 0
    XQueryExtension("Xie") = 1, opcode=129 event=64 error=0
    

    XieInitialize now tries "XIE" and falls back to "Xie".

Without both fixes xieperf either hangs or reports XIE not supported on this display! — neither reflects the server's actual XIE support.

Upstream bug: -showtechs alone prints usage

In xieperf.c the -showtechs branch sets showTechs but not foundOne, and main calls usage() when foundOne is false. So this is not a build problem:

./xieperf -display 192.168.99.2:0 -showtechs        # prints usage, exits
./xieperf -display 192.168.99.2:0 -showtechs -all   # works; -all sets foundOne

-showtechs only prints the capability list and exits before running any test, so the extra -all has no other effect.

The BadMatch gate: object->byte8 must equal 3

The 0x8 is now located. At 0x6BAF4 the lookup compares one byte of the object against the literal 3 that slot 19 hardcodes:

6BAEE MOVE *A13(0xff40),A11,0x0   ; = 3
6BAF2 MOVE *A7,A7,0x0             ; 8-bit field
6BAF4 CMP A7,A11
6BAF6 JREQ 0x6BBCE                ; equal -> selector dispatch
6BAF8 ... MOVK 0x8                ; else BadMatch

Measured at the CMP: GATE a7=00000001 a11=00000003. This is exactly the branch once "skipped" with pc = 0x3F55DE70 to make PUT_TILE report success — that was never a real path, and we now know what it bypassed.

byte8 is arm-controlled: the arm request sets it to the 16-bit field at payload bytes 8..9 (v12), measured v12 = 0,1,2,3 → byte8 = 0,1,2,3, with arm = OK for v16 ∈ {0,1} and arm = ERR8 for v16 ∈ {2,3}.

Correction: BadAccess(10) is the unarmed path, not progress. w9=0x100 with no arm gives put = ERR10 and the gate is never reached; with w9=0x300 the arm fails, so the put was simply unarmed. With v12=3 armed (byte8=3) the gate is still not reached and neither 0x8 site fires, yet put returns ERR8 — and sweeping the tile geometry (64×64, 1×1, 63,63, 128×128, …) changes nothing. So ERR8 has at least two origins in slot 19, and the one that fires with byte8=3 is earlier than the lookup.

Evidence: evidence/badmatch_gate_located.txt, evidence/arm_sets_byte8.txt.

byte8 cannot be raised by any request except create

Clean instrumentation (harness/mame/xie_min.lua, one breakpoint) and corrected field offsets (byte N = ptr + N*8, since ptr is a bit address). byte8 was previously read at ptr+8, which is byte 1 — those numbers were wrong and are superseded.

Verified mapping from create word 9:

w9 = 0x000 0x100 0x200 0x300 0x400
b8 =   0    1     2     3     4
b9 =   1    1     1     3     1        (b9 = (b8==3) ? 3 : 1)

Sweeping every other implemented request against a w9=0x100 (byte8=1) resource:

slot 43 (len 10) BadValue     b8 1 -> 1
slot 44 (len  5) BadValue     b8 1 -> 1
slot 40 (len  4) BadValue     b8 1 -> 1
slot 33 (len  4) BadValue     b8 1 -> 1
slot 24 (len  4) REPLY        b8 1 -> 1
slot 19 (len  8) BadAccess    b8 1 -> 1

No implemented request moves byte8. Only create does, via word 9's high byte. With the arm sweep (v12 = 0..6 × v16 = 0..3, 28 shapes) the measured incompatibility stands:

byte8=1 -> arm OK,  put = BadMatch   (gate: 1 != 3)
byte8=3 -> arm ERR8 (all 28 shapes), put = BadAccess (unarmed)

So within the client-reachable request set, byte8 cannot be raised to 3 while keeping the resource arm-able. The import cannot reach the handoff, src_base stays 0, and EXPORT has nothing to draw. Evidence: evidence/byte8_slot_sweep.txt.

Harness note: with several requests outstanding on one connection the reply stream misaligns (arm/create replies are often not read back). That does not affect these results — byte8 and the slot-19 error come from independent reads — but future runs should drain between requests or use one request per connection.

The wall in code: arm needs byte8==1, import needs byte8==3

Found by bisecting slot 20's body with generated bit addresses (harness/mame/xie_min9.lua, bit = 0x3F200000 + file*8 — hand arithmetic had produced several wrong addresses earlier), valid ids, no-reply model:

w9=0x100 v14=1 : JREQ_6AB82 -> TGT_6AC34 -> EXIT_6AFDC   -> SILENT (success)
w9=0x100 v14=2 : JREQ_6AB82 -> TGT_6AC34 -> E8_2_6AC5C   -> BadMatch
w9=0x300 v14=3 : JREQ_6AB82 -> TGT_6AC34 -> E8_4_6ACD4   -> BadMatch
w9=0x300 v14=1 : JREQ_6AB82 -> TGT_6AC34 -> E8_4_6ACD4   -> BadMatch

The discriminating check:

6ACBE MOVE *A11(0x40),A7,0x0     ; A7 := object->byte8
6ACC2 DSJS A7,0x6ACD2            ; byte8==1 -> OK chain
...
6ACCA MOVE *A9(0x68),A7,0x0      ; request byte 13 (mode)
6ACCE SUBK 0x3,A7
6ACD0 JRNE 0x6AD34               ; mode != 3 -> OK path
6ACD2 MOVK 0x8                   ; else BadMatch

So:

request requires
arm (slot 20) object->byte8 == 1 (0x6ACBE)
import (slot 19) object->byte8 == 3 (0x6BAF4)

and evidence/byte8_slot_sweep.txt shows no request changes byte8 except create (word9>>8). With the client-reachable request set, a resource cannot be both arm-able and import-eligible — which is why every configuration ends in BadMatch / BadAccess / BadIDChoice, src_base never becomes non-zero, and EXPORT has nothing to draw.

(The v14 value is validated separately at 0x6AC5C; mode != 3 is required at 0x6AD0.) Evidence: evidence/arm_requires_byte8_eq1.txt.

Closure: no request writes byte8

A write watchpoint on the created resource's byte8, armed across a complete cycle with valid ids and the no-reply model:

create  -> silent (success)          obj = 003FDC50
watch installed at 003FDC90          byte8 read-back = 1
arm1    -> silent (success)
put     -> BadMatch
arm2    -> BadMatch
export  -> silent (success)

byte8 WRITE EVENTS AFTER CREATE: 0

Nothing after create writes byte8. So the only route to byte8 == 3 is create word9 = 0x300, and such a resource cannot be armed; an unarmed resource's import returns BadAccess without reaching the gate.

Conclusion, with evidence: a byte8 transition into the importable state is not reachable from the client request set. The import cannot be driven, src_base stays 0, and EXPORT has nothing to draw. This closes the last request-level door in this investigation.

Residual possibilities are all outside that set: an internal path the watch never observed (nothing writes it in a normal cycle), an unimplemented minor, or the real DEC 2.3 sequence differing entirely — which its documentation would state. Evidence: evidence/byte8_write_watch_closure.txt.

The three residual items, tested

The previous closure was limited to one watchpoint on one resource; all three residuals are now tested (evidence/residual_items_tested.txt):

  1. Static search for internal byte-8 writers across every disassembled XIE handler. Exactly one candidate exists — slot 20 0x6ABF6..0x6AC00 (*A11(0x40) |= request payload byte 16) — and it is dead code: it is guarded by *A13(0x40) != 0 at 0x6AB82, while *A13(0x40) is only assigned later at 0x6AD92 from a freshly allocated params block. The measured fired-path confirms every arm goes 0x6AB82 → 0x6AC34 and never enters that block.
  2. Runtime watch on a w9=0x300 resource (the type the import wants) across four arm attempts, all BadMatch: 0 write events.
  3. Unimplemented minors as an alternate import path: all 21 unassigned minors × lengths 2,3,4,6,8,12 = 126 requests, every one BadRequest.

So there is no executing internal writer, no runtime writer, and no second import entry point. The byte8 transition into the importable state is unreachable from the client, src_base stays 0, and EXPORT has nothing to draw.

Slot 43 decoded: a reporting request, not an image source

Length is exactly 10 words. It reads two resource ids (payload words 0 and 1, looked up through 0x7EA6FA0 at 0x65640) plus two ROI fields (words 4 and 5), falling back to the resource's own width/height at *A13(0x1c0)+0x80/0xa0.

The "descriptor tables containing 3.0 / 438.0" are arrays of IEEE doubles, not descriptors — 0x3F533740 reads 00 00 00 00 00 00 08 40 = 3.0, and each MOVI points 8 bytes further into the same array. Each load is followed by two MOVE *A8+,*A14+ (one double) and a call into the printf family (0x7FC1CE6 / 0x7FC18F6 / 0x7FC1CDE), the same helpers used by the export timing print and by the logger at 0x7F638C2.

Tested live with valid ids and the no-reply model — 17 payload shapes, including doubles in every position, single-id and two-id forms, and ROI-shaped values: every one returned BadValue. Its exact ROI preconditions were not reconstructed, but its body is printf-dominated and nothing observed connects it to src_base or to any image data plane. It is therefore not credible as the image-source element. Evidence: evidence/slot43_decoded.txt.

Direct watch on the src_base slot, and slots 15 / 22

The src_base slot itself (rp+0x610) was watched, with rp captured at the export breakpoint (0x1B296, A11 = rp) — rp = 00913330, slot = 00913940.

create/arm1/put/export#1   put BadMatch, export#1 BadMatch
slot15 [id,0]              silent (success)
slot22 [id]                silent (success)
slot15 [id,id]             silent (success)
arm2, export#2             silent (success)

rp+0x610 after export#1 = 00000000
rp+0x610 after export#2 = 00000000

18 writes at 00913940; only XIE writer is pc=3F2D91F0 (file 0x1B23E, the known
`MOVE *A5(0x28),*A11(0x610)` copy) and it writes 0. The 0x55/0xB055/0x90E6
values are heap-fill and allocator metadata.

Slots 15 and 22 are permissive (they accept [id,0], [id,id], [id]) but neither produces a non-zero write to src_base. Also confirmed: export returns BadMatch if arm2 has not been issued, and is silent/success once it has — consistent with the earlier arm2 gate.

So the source image buffer is never produced, by any path watched. Evidence: evidence/srcbase_watch_and_slots15_22.txt.

Slot 43 excluded: it validates doubles and prints, it does not source images

BadValue comes from a double comparison against its table: it loads a double (0x3F533740 = 3.0, 0x3F533780, 0x3F533900, … successive 8-byte entries) and calls 0x7FC1CE6 / 0x7FC18F6 / 0x7FC1CDE:

658B8 CALLA 0x07FC1CDE
658BE JREQ  0x658CA     ; equal -> continue
658C0 MOVK  0x2,A7      ; else BadValue

Its one element-chain block copies each element's +0x1a8 into a frame-local array for printing (659E2 *A11(0x140) = *A7(0x1a8), with A11 = A13 + i*4), not into the resource. Every store goes to frame locals or the params block; it never writes resource+0x140, never writes rp+0x610, and allocates no image buffer.

Empirically, with a write watch on the real src_base (rp = 0091F090 → 0091F6A0), four slot-43 shapes all returned BadValue and the 64 watch events were all 00000000 or 00000055 (allocator fill) from export's own copy (pc=3F2D91F0) and allocator PCs — no non-zero image pointer.

So slot 43 is a reporting/describe request and cannot be the source of the image. This closes the last implemented slot whose acceptance condition was unresolved. Evidence: evidence/slot43_excluded.txt.

Slot 43: acceptance precondition still not satisfied (honest status)

The table is little-endian doubles (TMS34010 is LE): 0x3F533780 = 3.0, 0x3F5337C0 = 871.0, 0x3F533800 = 873.0, 0x3F533840 = 217.0, 0x3F533880 = 219.0.

Logging the two frame locals pushed as the operand pair at the double-compare sites (0x656B0 = "CMP740", 0x658B8 = "CMP900"):

[id,0,0,0,...]        BadValue   CMP740 a0=00000000 a1=00000000
[id,0,3.0hi,0,...]    BadValue   CMP740 a0=00000000 a1=00000000
[id,0,0,3.0hi,0,...]  BadValue   CMP740 a0=FFF7D264 a1=BFE800FF

CMP900 — the site that raises BadValue at 0x658C0 — never fired, so the rejection happens earlier than the comparison I was chasing, and the doubles are not read from the payload words tried.

Status of the slot-43 claim: it is excluded on code reading (a per-element reporting loop copying element+0x1a8 into a frame-local array, then printf) and on watched evidence that no slot-43 request produced a non-zero write to src_base — not on a completed, accepted request. Its exact acceptance precondition remains unknown. This is recorded rather than glossed: the implemented surface is not fully exercised until that precondition is found. Evidence: evidence/slot43_compare_operands.txt.

The render driver itself returns 2 to slot 43

Instrumented slot-43 tail (9 breakpoints, generated addresses, valid ids, isolated connection, create drained first):

T_LOOKUP_OK  0x65646              lookup returned 0
T_RENDER_RET 0x65BFE              a8 = 00000002   <-- the DRIVER returned 2
T_JREQ       0x65C02              non-zero path
T_FREE       0x65C06
(T_MERGE     0x65C46 did not fire)

So slot 43 calls 0x07E5B152 (file 0x1B152, the render/export driver) and that call returns 2 — which is what the client sees as BadValue. Neither of slot 43's own MOVK 0x2 sites is involved.

The single MOVK 0x2 in the driver's first 240 instructions (0x1B360) is an element type write (MOVB *A11,*A5(0x58)), not an error code, so the 2 comes from another exit of 0x1B152.

Caveat recorded: after T_FREE (0x65C06) the code falls through to 0x65C46, yet that breakpoint did not fire. Either it was dropped (despite only 9 armed) or the free call does not return normally; I did not resolve which, so the tail path after 0x65C06 is not fully established.

What is established: the failure is inside the render driver, not in slot 43's checks, not in the import lookup, and not the byte8 gate. An accepted slot 43 would drive the render path directly. Evidence: evidence/render_driver_returns_2.txt.

The manual (EK-V120I-IN-001) and the documented display path

docs/EK-V120I-IN-001_installing-DECimage-1200.txt — Appendix C, Table C–1 — gives the semantic model this project had been inferring:

  • supported requests: INIT_SESSION, SET/QUERY_OP_DEFAULTS, TERM_SESSION, CREATE_BY_VALUE, CREATE_BY_REFERENCE, DELETE_RESOURCE, QUERY_RESOURCE, EXPORT, FREE_EXPORT, QUERY_EXPORT, SET/ABORT_TRANSPORT, PUT_TILE, PUT_STREAM, SCALE, ROTATE, MIRROR, CROP, EXECUTE_PHOTOFLO, ABORT_PHOTOFLO, BIND_PHOTOFLO, TAP_PHOTOFLO, SELECT/QUERY_EVENTS;
  • "Always use pipelines"; elements limited to scale, crop, rotate, mirror, export, at most one of each, must always end with export, prefer that order, max 15;
  • XIEP_EXPORT "Creates a new server PIXMAP resource object", and images are displayed "by copying from a pixmap … using XCopyArea or XCopyPlane";
  • SET_OP_DEFAULTS: "Bits/pixel parameter value must be 1";
  • rotate only 0/90/180/270; width/height must be 0 or match the rotated source; tap only on an empty pipeline; no DCT; no server→client transport;
  • errors: "The client receives a Bad Request or Bad Match or Bad Value X error … Do not invoke an XIE protocol request that is implemented, but violates its restricted use."

That last point reframes much of this investigation: BadMatch/BadValue are documented, expected responses to a restriction violation (including "create non-bitonal image objects"), not bugs — and byte8 is most likely the bits/pixel field, which must be 1.

Implemented the documented display path (evidence/export_query_copyarea.txt): EXPORT → QUERY_EXPORT (op 3/4 reply, 64×64 + pointers + resource id) → XCopyArea → GetImage. The export context reply does not carry a usable pixmap XID — its id-like field varies per run (0x00920101, 0x00380101, 0x00330101, 0x00360101) and looks like a heap pointer; XCopyArea rejects each with BadDrawable. Window unchanged for solid-white and solid-black.