forked from arti/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).
1214 lines
55 KiB
Markdown
1214 lines
55 KiB
Markdown
# 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.
|