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).
19 KiB
XIE 2.3 — the minimum path to put one 1-bit image on the VT1200
See
GHIDRA_XIE_PIPELINE.md(final pass): the BadMatch gate is resource byte 5 bit 0, set from the create request's level word at byte 28; with a non-zero level entry the create (slot 8 op 2) produces an exportable class-2 resource. Exact 12/8/16-word payloads are there.
Third pass. Program vt1200_v22.bin (md5 b46a3e442ccb86134d2356bd70d22fd2).
The Ghidra session from passes 1–2 is gone; I re-opened the ROM and this
report's addresses come from the new session
a03f3d788b874b1a976a55417ab9f2d3 (same program, same base rom:00000000,
same language tms34010:LE:32:default). All addresses are file/byte offsets, so
they are identical to the earlier reports; mame_bit = 0x3F200000 + off*8.
New raw material: xie/ghidra-evidence/image_path/swapper_slotNN_HHHHHH.txt —
the 24 request byte-swappers, one per implemented slot. These give the exact
32-bit field map of every request, which is the main new evidence here.
Read with passes 1 and 2 (GHIDRA_XIE.md, GHIDRA_XIE_HANDLERS.md).
1. Executive summary
Nothing in the ROM creates an image resource through a plain single-id
request. Every 2-word and 3-word resource slot (11, 12, 13, 15, 22, 24, 33,
40, 44) begins with LookupPhotos and answers BadIDChoice for an unused id —
that was measured on the wire in pass 2 and it is confirmed structurally here.
Creation must therefore live inside the multi-operation classes 7, 8, 10, and
those are the only slots whose selector semantics are still open.
What is now nailed down:
- Request field maps for all 25 slots, from the byte-swappers (§3). Slots
7, 9 and 10 carry nothing but 32-bit words; slots 12 and 22 are literally
{header, id}. - Slot 19 is the tile-put request. It clamps (x, y), adds (w, h) to get the
right/bottom edges, rejects a fully clipped rectangle with
BadValue, iteratescountelements writing x and y into per-element records, and uses the request tail as pixel data. Layout:{id@4, x@8, y@12, w@16, h@20, count@24}+ payload (§4). This is DEC'sXIEP_PUT_TILE("Transports random tiles of uncompressed image data from the client to the server") — confidence high on structure, medium on the name. - Slot 21 is the export candidate: 16 words,
{id@4, then ten 32-bit words at 8…44, two 16-bit sub-words}, and it starts by resolving the id throughLookupPhotos— i.e. "take this image resource and produce something from it". DEC:XIEP_EXPORT"Creates a new server PIXMAP resource object". - Slots 20 and 43 are the only ones shaped like a fixed descriptor (35 words / 10 words); slot 20 additionally seeds a 25-element notion.
The biggest blocker is a single missing fact: which request creates the image resource, and what class code it wants. Without it nothing can be put or exported. Second blocker: whether the export targets a client-created Pixmap (5.0 model) or creates one itself (DEC wording). §7 gives the exact probes that settle both in four requests.
2. Slot table — resource / transport / export family
"fields" is the set of 32-bit request words proven by the byte-swapper (w =
word index, byte offset = 4×w), so it is the wire layout, not an inference.
| slot | best name | conf | 5.0 / DEC counterpart | words | 32-bit fields (byte off) | reply |
|---|---|---|---|---|---|---|
| 7 | resource create class — op 1/2 = create-by-value / by-reference | [I] | XIEP_CREATE_BY_VALUE, XIEP_CREATE_BY_REFERENCE |
4 | 4 (op), 8 (id), 12 (class) | no |
| 8 | resource class, 4 sub-handlers (ops 1,2,4,5,6) | [I] | resource services | ≥3 | 4, 8, 12, 16, 20 | no |
| 9 | 3-word op on a resource | [U] | — | 3 | 4, 8 | no |
| 10 | 5-operation class (query/export shaped) | [I] | XIEP_QUERY_RESOURCE / XIEP_QUERY_EXPORT / XIEP_EXPORT |
3 | 4 (op), 8 | yes |
| 11 | resource op with a second parameter | [U] | — | 3 | 4, 8 | no |
| 12 | delete / free (id only) | [I] | XIEP_DELETE_RESOURCE, XIEP_FREE_EXPORT |
2 | 4 | no |
| 13 | delete / free (id only) | [I] | as above | 2 | 4 | no |
| 14 | resource op, id + one long + two half-words | [U] | — | 4 | 4, 8 | no |
| 15 | pipeline op (photoflo family) | [U] | XIEP_BIND_PHOTOFLO? |
3 | 4, 8 | no |
| 18 | pipeline op | [U] | XIEP_EXECUTE_PHOTOFLO? |
≥4 | 4, 8 | no |
| 19 | PUT_TILE — x/y/w/h + counted payload | [C] structure | XIEP_PUT_TILE |
≥8 + data | 4, 8, 12, 16, 20, 24 | no |
| 20 | fixed descriptor (photoflo definition) | [I] | XIEP_EXECUTE_PHOTOFLO / CREATE_PHOTOFLO |
35 | 4, 8, 16 (+16-bit at 12) | no |
| 21 | EXPORT — id + big parameter block | [I] | XIEP_EXPORT |
16 | 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44 | no |
| 22 | delete / free (id only) | [I] | XIEP_DELETE_RESOURCE |
2 | 4 | no |
| 24 | QUERY_RESOURCE (only photo slot with a reply) | [C] | XIEP_QUERY_RESOURCE |
4 | 4, 8 (+16-bit at 12) | yes |
| 33 | generic photo op, class from the request | [I] | XIEP_QUERY_EXPORT? |
4 | 4 (id), 8 (class), 12 | no |
| 40 | generic photo op, class from the request | [I] | — | 4 | 4, 8 | no |
| 43 | large photo op (10 words) | [U] | XIEP_PUT_STREAM? |
10 | 4, 8, 16, 20, 24 (+ one 16-bit field near 14) | no |
| 44 | generic photo op, class from the request | [I] | — | 5 | 4, 8, 12, 16 | no |
Session slots (1–6) are unchanged from pass 2: 1 INIT_SESSION, 2
TERM_SESSION, 3 QUERY_OP_DEFAULTS, 4 SET_OP_DEFAULTS, 5 mode-selecting
setter, 6 QUERY_EVENTS.
3. How the field maps were recovered [C]
For a byte-swapped client, XieSProc calls the per-slot stub; each stub
byte-swaps stuff->length and then every 32-bit request word in place, then
re-dispatches into XieProc. One 32-bit word at byte B produces three MOVB
instructions:
rom:00069c04 MMTM SP,0x74 ; slot 11's swapper
rom:00069c0a MOVE *A13(0xffe0),A11,0x1
rom:00069c0e MOVE *A11(0x40),A10,0x1 ; stuff
rom:00069c12 MOVB *A10(0x10),*A9 ; ---- swap stuff->length
rom:00069c16 MOVB *A10(0x18),*A10(0x10)
rom:00069c1c MOVB *A9,*A10(0x18)
rom:00069c20 MOVB *A10(0x20),*A9 ; ---- word at byte 4
rom:00069c24 MOVB *A10(0x38),*A10(0x20)
rom:00069c2a MOVB *A9,*A10(0x38)
rom:00069c2e MOVB *A10(0x28),*A9 ; (bytes 5/6 of the same word)
rom:00069c32 MOVB *A10(0x30),*A10(0x28)
rom:00069c38 MOVB *A9,*A10(0x30)
rom:00069c3c MOVB *A10(0x40),*A9 ; ---- word at byte 8
...
rom:00069c5e MOVE *A10(0x8),A11,0x0 ; minor opcode
rom:00069c64 ADDI 0xd3f90,A11 ; -> XieProc's proc table
Applied to all 24 stubs, the swapped-word sets are:
| slot | 32-bit words at byte | reading |
|---|---|---|
| 1 | — (16-bit at 4 and 6 only) | version pair {major, minor} |
| 2 | — | no arguments at all |
| 3 | 4 | one 32-bit argument (the 2.3 handler ignores it) |
| 4 | 4 | one 32-bit value → per-client record |
| 5 | — | every argument is ≤16 bits |
| 7 | 4, 8, 12 | {op, id, class} |
| 8 | 4, 8, 12, 16, 20 | parameter run |
| 9, 10 | 4, 8 | {op, value} |
| 11 | 4, 8 | {id, param} |
| 12, 13, 22 | 4 | {id} |
| 14 | 4, 8 | {id, param} + half-words |
| 15 | 4, 8 | {a, b} |
| 18 | 4, 8 | {a, b} |
| 19 | 4, 8, 12, 16, 20, 24 | {id, x, y, w, h, count} |
| 20 | 4, 8, 16 | plus 16-bit fields at 12 |
| 21 | 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44 | id + ten parameters |
| 24 | 4, 8 | {id, param} |
| 33 | 4, 8, 12 | {id, class, param} |
| 40 | 4, 8 | {id, class} |
| 43 | 4, 8, 16, 20, 24 (+16-bit at 14) | |
| 44 | 4, 8, 12, 16 |
This is why slots 12/13/22 are "delete shaped" and 33/40/44 are "generic photo
handle" slots: 33 passes stuff->class (byte 8) as the last argument to
LookupPhotos, so the client names the resource class, while 11/12/13/14/15/
21/22/24 hard-code it (0xFFFF = any, or 0).
4. Slot 19 = the tile-put path [C for structure]
Length check and the identification call:
rom:0006a782 MOVE *A9(0x10),A11,0x0 ; stuff->length
rom:0006a786 SUBK 0x8,A11
rom:0006a788 JRNC 0x0006a790 ; length >= 8 words
rom:0006a78a MOVK 0x10,A8 ; else BadLength
rom:0006a790 MOVE A14,-*SP,0x1
rom:0006a792 MOVE *A9(0xc0),*A14+,0x1 ; arg = stuff word @ byte 24
rom:0006a796 MOVK 0x3,A11
rom:0006a798 MOVE A11,*A14+,0x1 ; arg = 3
rom:0006a79a MOVE *A9(0x20),*A14+,0x1 ; arg = id @ byte 4
rom:0006a7a2 MOVE A7,*A14+,0x1
rom:0006a7aa MOVE *A13(0xffe0),*A14+,0x1
rom:0006a7ae CALLA 0x07eab966 ; 0x6B966 (pipeline attach, arg 3)
Then the rectangle algebra, which is what makes this the put path:
rom:0006a7be MOVE *A9(0x40),A11,0x1 ; w1 = x (byte 8)
rom:0006a7c2 JRLE 0x0006a7ca ; clamp to 0
rom:0006a7cc MOVE A11,*A13(0x80) ; local x
rom:0006a7d0 MOVE *A9(0x60),A7,0x1 ; w2 = y (byte 12)
rom:0006a7de MOVE A7,*A13(0xa0) ; local y
rom:0006a7e2 MOVE *A9(0x80),A7,0x1 ; w3 = width (byte 16)
rom:0006a7e6 MOVE *A9(0x40),A5,0x1 ; + x
rom:0006a7ea ADD A5,A7
rom:0006a7ee MOVE *A5(0x80),A5,0x1 ; compare with dst width -> clip
...
rom:0006a812 MOVE *A9(0xa0),A7,0x1 ; w4 = height (byte 20)
rom:0006a816 MOVE *A9(0x60),A5,0x1 ; + y -> clip
...
rom:0006a84e JRLE 0x0006a854
rom:0006a850 MOVE A5,A8
rom:0006a852 JRGT 0x0006a87c ; rectangle non-empty -> go on
rom:0006a856 MOVK 0x2,A5 ; else error 2 = BadValue
rom:0006a85a MOVE *A9(0x20),*A14+,0x1 ; (client, major, minor, id, 2)
rom:0006a870 CALLA 0x07effa5e ; SendErrorToClient
and the payload loop:
rom:0006a88c MOVE *A9(0xc0),*A13(0x40) ; loop counter = word @ byte 24
rom:0006a896 JREQ 0x0006aa28 ; zero elements -> skip
rom:0006a89e BTST 0x0,A11 ; bit 0 of that word = direction/flag
rom:0006a8ac SLL 0x5,A5 ; element stride 32 bits
rom:0006a8b0 MOVE *A5(0x160),A5,0x1
rom:0006a8b4 MOVE *A13(0x80),*A5(0xa8),0x1 ; element.x = x
rom:0006a8c6 MOVE *A13(0xa0),*A5(0xe8),0x1 ; element.y = y
rom:0006a8d8 MOVE *A13(0x80),A7,0x1
rom:0006a8dc MOVE *A13(0xc0),A11,0x1
rom:0006a8e0 ADD A11,A7 ; x += width
Layout (bytes): 0 hdr | 4 id | 8 x | 12 y | 16 width | 20 height | 24 count | 28 two 16-bit fields (not byte-swapped as a long) | 32… payload.
Minimum length 8 words = 32 bytes, i.e. header only; pixel bytes follow and are
covered by the same length.
Not proven: whether count counts bytes, rows, or tile records, and whether the
payload is raw 1-bit rows packed 8-per-byte or bit-addressed. The BTST 0x0
on the same word suggests a flags/count word; the loop's stride-32-bit element
walk suggests it is a count of tiles, not bytes.
5. Export candidates
Slot 21 — 16 words, resolves the id then works on it:
rom:00062d58 SUBK 0x10,A11 / JREQ ; exactly 16 words
rom:00062d68 MOVE *A9(0x20),*A14+,0x1 ; arg = id @ byte 4
rom:00062d6c MOVE A13,A7 / MOVE A7,*A14+ ; out pointer 1
rom:00062d72 MOVE A13,*A14+ ; out pointer 2
rom:00062d78 CALLA 0x07ea6fa0 ; LookupPhotos(client, id, &a, &b, 0)
rom:00062d84 JRNE 0x000630ea ; lookup error -> return it
rom:00062d88 MOVE *A13(0x20),A11,0x1 ; the resolved resource
It then walks the ten 32-bit parameters at bytes 8…44. A 44-byte parameter block after a resource id is exactly what an export descriptor looks like (destination pixmap, x, y, w, h, visual/format, stride, flags). [I]
Slot 10 — five operations, the only multi-op slot that replies, so if 2.3
has a "query export context" it is one of its five ops. Its request is only
three words {op@4, value@8}, so its ops must be id-free or take the id in
value. [I]
Slots 33/40/44 pass a client-chosen class to LookupPhotos and then a
parameter; they are the "do something to resource of class " form and
are the most likely home of export-by-class. [I]
6. Best sequence
Everything is little-endian; len is in words including the 4-byte header.
Step confidence in brackets.
[0] connect, QueryExtension("Xie") -> opcode 129 [C]
[1] INIT_SESSION minor 1, len 2
02 00 03 00 (client speaks 2.3; any value accepted)
-> 64-byte reply, major 2 minor 3 [C]
[2] CREATE RESOURCE minor 7, len 4 [I]
op = 01 00 00 00
id = 2a 00 00 00 (0x2A = our photomap id)
class = ?? ?? ?? ?? (see probe P1)
-> expect Success (no packet). BadRequest with bad=0x2a means the
class word or op is wrong.
[3] PUT_TILE minor 19, len 8 + payload [I-high]
00 00 00 00 | 2a 00 00 00 | 00 00 00 00 | 00 00 00 00 |
40 00 00 00 | 40 00 00 00 | <?count?> | 00 00 00 00 |
<512 bytes of 1-bit pixel data = 128 words>
total len = 136
-> Success, or BadValue(2) if the rectangle clips away entirely,
BadIDChoice(14) if the id did not resolve.
[4] EXPORT minor 21, len 16 [I]
id = 2a 00 00 00, then ten parameter words (probe P2)
-> Success expected; the pixmap id is client-supplied (DEC wording)
[5] TERM_SESSION minor 2, len 1 [C]
-> no packet; just close.
6.1 The 64×64 checkerboard
64 px × 64 rows at 1 bit = 8 bytes/row = 512 bytes = 128 words. A checkerboard
of 8×8 blocks is a single repeated 32-bit pattern per byte-pair; the simplest
generator is "row r, byte b: 0xAA if ((r/8 + b) & 1) else 0x55".
row 0..7 : AA 55 AA 55 AA 55 AA 55 (per byte, 8 bytes)
row 8..15 : 55 AA 55 AA 55 AA 55 AA
...
Which of the two bit orders is "on" does not matter for a first display test.
6.2 Shorter path, if it exists
If XIEP_EXPORT really creates the pixmap itself, step [4] returns the pixmap
id in the slot-10 reply and the client can skip creating a pixmap. If instead
the 5.0 model holds, the client must first
CreatePixmap(depth=1, 64x64) and pass that XID in step [4]; then
XCopyArea(pixmap -> window) displays it. Both variants use the same four
XIE requests; only the pixmap's origin differs.
7. Probes that settle the open fields (for the worker with server access)
Each probe is one connection: INIT (minor 1) → probe → TERM (minor 2).
| id | request | variants | observation |
|---|---|---|---|
| P1 | create: minor 7, len 4, {op, id=0x2A, class} |
class ∈ {0, 1, 2, 3, 4}, op ∈ {1, 2} | the pair that returns Success is create + the image class. BadRequest (code 1) with bad=0x2a ⇒ id rejected; BadIDChoice (14) ⇒ op is a lookup, not a create |
| P2 | export: minor 21, len 16, {id=0x2A, …} |
zero-fill the ten parameters, then vary word at byte 8 (likely the destination pixmap or a format) | BadIDChoice ⇒ slot 21 is not the export from this id; Success ⇒ export, and the reply/absence of a reply tells whether the pixmap comes from the client |
| P3 | put: minor 19, len 8 (header only, no data) and len 136 with a checkerboard | count word at byte 24 ∈ {1, 64, 128, 512} | Success on the first, and BadValue on a zero-size rect, confirms the geometry; a length-related error identifies the count unit |
| P4 | query after put: minor 24, len 4, {id=0x2A, 0} |
— | a reply whose words change after P3 proves the data landed (the only photo slot that can reply) |
| P5 | delete: minor 12 and 22, len 2, {id=0x2A} |
— | whichever returns Success deletes the resource (the other is FREE_EXPORT) |
Order matters: P1 first (nothing else can work without a resource), then P3, then P4 to prove the pixels landed, then P2.
8. Server-side constraints visible in the ROM
- Every handler checks
stuff->lengthexactly (or>=for 8, 18, 19); a wrong length always yieldsBadLength(16) withminor_op = 0(pass 1 §7). A 2.3 client must therefore get every length exactly right. - Slot 19's clipping uses the destination resource's width/height read from
the resolved resource record (
*A5(0x80),*A5(0xa0)), so the image resource must carry real dimensions before a put. That is the strongest argument that the create step (P1) supplies width/height somewhere — note slot 7's request is only{op, id, class}, so dimensions most likely come from slot 8 or 21 rather than 7. This is the main open risk in the sequence above. - Slot 19 rejects a fully clipped rectangle with
BadValue(2) and a bad id withBadIDChoice(14), both carrying the real minor opcode. - Export target type (Pixmap vs Window) is not visible in any handler I read; DEC's manual says a PIXMAP resource is created. [U]
- 1-bit only: the terminal is
StaticGray, effective depth 1 (live probe), and DEC's restrictions say "resource-manipulation parameter values are restricted to those compatible with bitonal images".
9. Why this should display
XIE 2.3 on this server is the DEC/AGE-Logic image pipeline: a resource is
created and filled by transport (PUT_TILE), then exported into a pixmap the
X server owns. Once the pixels are in a pixmap, an ordinary X client can
XCopyArea them into the Terminal Manager's window — no XIE involvement after
the export. The three steps map one-to-one onto DEC's documented functions
(XIEP_CREATE_* → XIEP_PUT_TILE → XIEP_EXPORT), the field maps come from the
server's own byte-swappers rather than from guesswork, and slot 19's geometry
code is unmistakably a clipped rectangle blit. The uncertainty is in which
slot implements create, not in whether the pipeline exists.
10. Confirmed / Inferred / Unknown
Confirmed
- Request field maps for all 25 implemented slots (§3), from the 24 byte-swap stubs.
- Slot 19 is a clipped-rectangle tile put with
{id, x, y, w, h, count}and a payload, minimum 8 words (§4). - Slots 12, 13 and 22 are
{header, id}only; 11/14/15/18/24 take{id, one more long}; 33 takes{id, class, param}. - Slot 21 resolves an id and then reads ten 32-bit parameters.
- Slots 1–6 are the session/event group (pass 2).
Inferred
- Slot 7 (ops 1/2) is create-by-value / create-by-reference.
- Slot 21 is export; slot 10 hosts query-export; 12/22 are delete/free.
- Slot 19's
countcounts tile records, not bytes.
Unknown
- Which slot creates the resource and where the dimensions live — the blocker.
- Pixmap vs Window as the export target, and who allocates the pixmap id.
- Whether the payload of slot 19 is packed rows or bit-addressed data.
- The class code values (
0vs0xFFFFvs a client number).
11. Reproduction
program_list_open→ empty; re-open withprogram_open(path="/home/arti/code/k-space/vt1200/vt1200_v22.bin", language="tms34010:LE:32:default", project_location=…, read_only=false)→ sessiona03f3d788b874b1a976a55417ab9f2d3.- Swapper listings are pre-extracted in
xie/ghidra-evidence/image_path/swapper_slotNN_HHHHHH.txt. - Handler bodies:
xie/ghidra-evidence/handlers/slotNN_HHHHHH.txt(pass 2). - To re-derive a field map, collect the
MOVB *(A10|A11)(0xNN),*A9offsets in a stub: a 32-bit word at byte B shows up as offsets8B, 8B+8, 8B+16, 8B+24; the length swap is the pair at0x10/0x18and must be ignored.