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

1214 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
- <https://www.x.org/releases/X11R6.1/tars/xc-1.tar.gz>
— `xc/programs/xieperf`, `xc/lib/XIE`, `xc/include/extensions/XIE*.h`,
`xc/programs/Xserver/XIE`
- <https://www.x.org/releases/X11R6.1/tars/xc-3.tar.gz>
— `xc/doc/hardcopy/XIE` (documents)
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:
- Browsable single files (Debian's `vnc4` source package bundles an XFree86
4.3-era `xc`): <https://sources.debian.org/src/vnc4/4.1.1%2BX4.3.0-37.6/unix/xc/programs/xieperf/>
and <https://sources.debian.org/src/vnc4/4.1.1%2BX4.3.0-37.6/unix/xc/lib/XIE/>
- XFree86 3.3.6 mirror: <https://github.com/ljmccarthy/XFree86-3.3.6>
- X11R6 (1994) and later releases: <https://www.x.org/releases/X11R6/>,
<https://www.x.org/releases/X11R6.1/>, <https://www.x.org/releases/X11R6.4/>
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**:
```sh
./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).
```sh
./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:
```sh
./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:
```sh
./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.