Files
vt1200/xie/GHIDRA_XIE_PIPELINE.md
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

15 KiB
Raw Permalink Blame History

XIE 2.3 — photoflo / export layer (final pass)

See GHIDRA_XIE_IMPORT.md: PUT_TILE's lookup helper 0x6B966 requires resource byte 36 non-zero and byte 40 zero, a band index at request byte 24 (not a count), and slot 7 takes type@8 / id@12 — not two ids.

Program vt1200_v22.bin (md5 b46a3e442ccb86134d2356bd70d22fd2), Ghidra session a03f3d788b874b1a976a55417ab9f2d3. Addresses are file offsets; mame_bit = 0x3F200000 + off*8; CALLA operand − 0x7E40000 = file offset.

Companions: GHIDRA_XIE.md, GHIDRA_XIE_HANDLERS.md, GHIDRA_XIE_IMAGE_PATH.md. Raw listings: xie/ghidra-evidence/pipeline/ (create_slot8_op12, resource_init_6CA82, export_slot21, put_slot19, dd_input_6B640).

Offset convention. Every offset in this code is a bit offset from the pointer. *A9(0x20) is request byte 4; *A7(0x28) with a 1-bit field is byte 5, bit 0. The earlier reports' "resource +0x28 / +0x29" therefore mean bit 0x28 = byte 5 bit 0, and neighbouring bits of byte 5.


1. The BadMatch is now explained and fixed [C]

Slot 21 refuses to export unless resource byte 5, bit 0 is set:

rom:00062d88 MOVE *A13(0x20),A11,0x1     ; A11 = resolved resource
rom:00062d8c SETF 0x1,0x0,0x0            ; 1-bit field
rom:00062d8e MOVE *A11(0x28),A7,0x0      ; A7 = resource bit 0x28
rom:00062d92 JRNE 0x00062dbc             ; set -> continue
rom:00062d96 MOVK 0x8,A7                 ; else error 8 = BadMatch
rom:00062d9a MOVE *A9(0x20),*A14+,0x1    ;   bad value = resource id
rom:00062db0 CALLA 0x07effa5e            ;   SendErrorToClient(...)

That bit is written by the create handler, from the request's level array:

rom:0006cbfe MOVE *A9,A9,0x1             ; A9 = levelArray[i]
rom:0006cc00 JRNE 0x0006cc06
rom:0006cc02 SUB  A9,A9                  ;   0
rom:0006cc04 JR   0x0006cc08
rom:0006cc06 MOVK 0x1,A9                 ;   1
rom:0006cc08 SETF 0x1,0x0,0x0
rom:0006cc0a MOVE A9,*A7(0x28),0x0       ; resource bit 0x28 = (entry != 0)

The loop runs over the request's own words, so:

A create whose request word at byte 28 is zero produces a resource with byte 5 bit 0 = 0, which no export can ever use.

Every create probe so far zero-filled byte 28 — that is why slot 21 always answered BadMatch, whatever the 8-bit fields were set to. Setting byte 28 to a non-zero "levels" value (e.g. 2) is the fix.


2. Resource record and class codes [C]

Filled by the create path (0x6CA82), which is reached from slot 8 ops 1 and 2:

rom:0006caf2 MOVE *A13(0xfec0),A11,0x1   ; arg = (op == 2) ? 1 : 0
rom:0006caf6 JREQ 0x0006cafc
rom:0006caf8 MOVK 0x2,A11                ; op 2 -> class byte = 2
rom:0006cafa JR   0x0006cafe
rom:0006cafc MOVK 0x3,A11                ; op 1 -> class byte = 3
rom:0006cafe MOVB *A11,*A9(0x20)         ; resource byte 4 = class
rom:0006cb02 MOVK 0x1,A11
rom:0006cb06 MOVE A11,*A9(0x30),0x0      ; resource bytes 6..7 = 1  (16-bit)
rom:0006cb0a MOVB *A13(0xffe0),*A9(0x40) ; byte 8  = request byte 41
rom:0006cb12 MOVE *A9(0x40),A7,0x0
rom:0006cb16 SUBK 0x3,A7
rom:0006cb1a MOVK 0x3,A7                 ; byte 9  = (req[41] == 3) ? 3 : 1
rom:0006cb20 MOVB *A7,*A9(0x48)
rom:0006cb30 MOVB *A13(0xff60),*A9(0x58) ; byte 11 = request byte 42
rom:0006cb36 MOVB *A13(0xff40),*A9(0x60) ; byte 12 = request byte 43
rom:0006cb3c MOVB *A13(0xff20),*A9(0x50) ; byte 10 = request byte 44
rom:0006cb42 MOVE *A13(0xff00),*A9(0x80),0x1 ; bytes 16..19 = width  (32-bit)
rom:0006cb48 MOVE *A13(0xfee0),*A9(0xa0),0x1 ; bytes 20..23 = height (32-bit)
resource byte type value / meaning
4 u8 class: 2 = op-2 resource, 3 = op-1 resource
5 bit 0 bit "levels defined" — gates export (BadMatch) and bound checks
6 u16 1
8 u8 request byte 41 (band count selector)
9 u8 3 if req[41]==3 else 1 — number of level entries examined
10 u8 request byte 44
11 u8 request byte 42
12 u8 request byte 43
16 u32 width (from request byte 12)
20 u32 height (from request byte 16)
24, 28 u32 two 32-bit values from the create request
44 ptr per-band array, records of 0x20 bytes (0x160 bits), one per band

Class ↔ behaviour, cross-checked against the live probes:

  • class 2 — the image resource. PUT_TILE (slot 19) resolves this class (it answers BadIDChoice for class 3 and BadAccess for class 2 with bit0x28 clear).
  • class 3 — the op-1 resource; QUERY (slot 24) answers BadAccess on it.
  • The class argument that slots 33/40/44 pass to LookupPhotos is this same byte-4 value, supplied by the client.

The per-band level count maps to bits per pixel (0x6cb7c…0x6cbb0):

entry == 0      -> 0 bits (empty band)
entry <= 2      -> 1 bpp
entry <= 0x100  -> 8 bpp
entry <= 0x10000-> 16 bpp
rowBytes = ALIGN(entry * width, 8)          ; 0x6cbb4 MPYU / ADDK 7 / ANDN 7

So entry = 2 is exactly "1 bit per pixel" — the value to use for our test image.


3. Slot 8 — create request, 12 words [C]

Both ops share one handler (0x678DA); the op word only selects the class byte. length must be exactly 12 words (rom:000678f8 SUBK 0xc / JREQ).

word byte field notes
0 0 header reqType=129, opcode=8, length=12
1 4 op 2 → class 2 (image), 1 → class 3
2 8 id new resource id; must satisfy (id & 0xFFF00000) == client->0x20, have bit 19 clear (BTST 0x13), and must not already exist
3 12 width must be non-zero, else BadValue (2)
4 16 height must be non-zero, else BadValue (2)
5 20 parameter passed by address to 0x1238C2 before creation
6–9 24–39 level array (up to 3 longs at byte 28, 32, 36) entry != 0 sets resource bit 0x28; values 1–2 = 1 bpp
10 40 (unused)
— 41 u8 band count 3 → three entries examined, anything else → one
— 42,43,44 u8 params copied to resource bytes 11, 12, 10

Validation order (0x678DA): length == 12 → req[12] and req[16] non-zero (else BadValue with bad = id) → id in the client's range and bit 19 clear (else BadIDChoice) → id must be absent (else BadIDChoice) → create (0x6CA82; failure is BadAlloc, code 11).

Note rom:00067902 MOVE *A9(0x60),A7,0x1 / JREQ — req[12] and req[16] must both be non-zero, which is also what makes the live "all-zero create" behaviour consistent.


4. Slot 21 — export, 16 words [C for the rectangle, I for the rest]

rom:00062d8e MOVE *A11(0x28),A7,0x0      ; resource bit 0x28 must be set  -> BadMatch
rom:00062dbc MOVE *A9(0x80),A7,0x1       ; A7 = req[16]
rom:00062dc0 MOVE *A11(0x80),A11,0x1     ; A11 = resource width
rom:00062dc4 CMP A11,A7 / JRC            ; if req[16] > width  -> BadValue
rom:00062df8 MOVE *A9(0xa0),A7,0x1       ; req[20] vs resource height -> BadValue
rom:00062e30 MOVE *A9(0xc0),A11,0x1      ; req[24] must be non-zero
rom:00062e36 MOVE *A9(0xe0),A11,0x1      ; req[28] must be non-zero
rom:00062e76 MOVE A11,*A13(0x60)         ; x0 = req[16]
rom:00062e88 MOVE A7,*A13(0x80)          ; y0 = req[20]
rom:00062e9a ADD A7,A5 ; MOVE A5,*A13(0xa0)   ; x1 = x0 + req[24]
rom:00062eb2 ADD A5,A7 ; MOVE A7,*A13(0xc0)   ; y1 = y0 + req[28]
rom:00062f7e CALLA 0x07ea3432            ; prepare/validate export
rom:0006301c MOVI 0xc8,A5 / CALLA 0x07f63a74  ; allocate 200-byte export context
rom:0006306e MOVK 0xb,A8                 ; failure -> BadAlloc (11)
word byte field
0 0 header, length = 16
1 4 id of the image resource
2, 3 8, 12 32-bit fields (byte-swapped; not read on the paths traced — reserved)
4 16 x — must be ≤ resource width
5 20 y — must be ≤ resource height
6 24 width — must be non-zero
7 28 height — must be non-zero
8 32 parameter (*A9(0x100), used in the offset maths)
9 36 parameter (*A9(0x120))
10–11 40, 44 parameter / flags
12–15 48… within the 16 words; not read on the traced paths

The handler allocates a 200-byte export context itself (MOVI 0xc8 → allocator 0x63A74), so it is the server, not the client, that owns the export state. Whether the destination is a client-supplied Pixmap or a server-created one is decided inside 0x63432 / 0x645FE, which I did not fully trace — [U], and it is the only remaining unknown in the path.


5. Slot 19 — put tile (unchanged, re-confirmed)

{id@4, x@8, y@12, w@16, h@20, count@24, flags@28}, minimum 8 words, payload follows; geometry is clipped against the resource's width/height (bytes 16/20) and an empty rectangle is BadValue. The data lands through the device-dependent input path 0x6B640 (dd_input_6B640 listing), which copies 0xE longs plus a byte from a per-band record before calling the DDXIE vtable.

Slot 19 answers BadAccess on a class-2 resource whose bit 0x28 is clear — the same gate as export. So one create with a non-zero level entry unlocks both PUT and EXPORT.


6. Slots 20 and 43 — status

  • Slot 20 (exactly 35 words, seeds 25, loops over 0x20-byte element records at 0x32…): consistent with a photoflo definition carrying up to three element records. Not decoded here; not needed for the minimal path if PUT_TILE + EXPORT work.
  • Slot 43 (10 words; compares request fields against the static table at 0x3F533740…; succeeds silently on a class-3 resource with a zero payload): most likely a bind/tap with no data. Not needed for the minimal path.

Both are only needed if the direct put→export route is refused.


7. Minimal sequence for a 64×64 1-bit image [I, but every field is now sourced]

base = the connection's X resource base (0xC00000 in the observed setup), id = 0xC0AAAA (bit 19 clear).

[0] connect; QueryExtension "Xie" -> 129                              [C]

[1] INIT_SESSION     minor 1, len 2
      02 00 03 00                                                     [C]

[2] CREATE (class 2) minor 8, len 12            12 words = 48 bytes   [C]
      word1  02 00 00 00      op = 2  (class 2, the image resource)
      word2  aa aa c0 00      id = 0xC0AAAA
      word3  40 00 00 00      width  = 64
      word4  40 00 00 00      height = 64
      word5  00 00 00 00      parameter (0)
      word6  00 00 00 00      \
      word7  02 00 00 00      |  level array: entry[0] = 2  -> 1 bpp,
      word8  00 00 00 00      |  and entry != 0 sets resource bit 0x28
      word9  00 00 00 00      /
      word10 00 00 00 00      byte40 unused, byte41 = 01 (one band),
                              byte42/43/44 = 00
      -> Success (no packet). BadValue(2) bad=id ⇒ width/height zero.
         BadIDChoice(14) ⇒ id out of range, bit 19 set, or already exists.

[3] PUT_TILE         minor 19, len 8 + payload                        [I]
      word1  aa aa c0 00      id
      word2  00 00 00 00      x = 0
      word3  00 00 00 00      y = 0
      word4  40 00 00 00      w = 64
      word5  40 00 00 00      h = 64
      word6  01 00 00 00      count = 1        (see probe P2)
      word7  00 00 00 00      flags
      then 512 bytes (128 words) of 1-bit pixels
      total length = 8 + 128 = 136
      -> Success, BadValue(2) if the rect clips away, BadAccess if bit0x28
         is clear, BadIDChoice if the id is class 3 / absent.

[4] EXPORT           minor 21, len 16                                 [I]
      word1  aa aa c0 00      id
      word2  00 00 00 00      reserved
      word3  00 00 00 00      reserved
      word4  00 00 00 00      x = 0        (<= 64)
      word5  00 00 00 00      y = 0        (<= 64)
      word6  40 00 00 00      w = 64       (non-zero)
      word7  40 00 00 00      h = 64       (non-zero)
      word8..15 = 0
      -> BadMatch(8) if step 2 left byte 28 zero; BadValue(2) if x>width or
         y>height or w/h zero; BadAlloc(11) if the 200-byte context fails.

[5] TERM_SESSION     minor 2, len 1                                   [C]

64×64 at 1 bpp = 8 bytes per row = 512 bytes; a checkerboard of 8×8 blocks is 0xAA for even (row/8 + byte) and 0x55 for odd, repeated over 8 rows.

If step [4] then reports the pixmap, display it with an ordinary XCopyArea from that pixmap into a window — no further XIE traffic. If instead the destination must be supplied, the pixmap id belongs in one of words 8, 9, 10 or 11 (bytes 32/36/40/44); that is what probe P3 decides.


8. Confirmed / Inferred / Unknown

Confirmed

  1. Resource class byte: 4 → 3 (op 1) or 2 (op 2) — 0x6cafe.
  2. Resource byte 5 bit 0 = "level entry non-zero", written at 0x6cc0a from the create request's level array at byte 28 — this is the BadMatch gate.
  3. The create request is 12 words with width@12, height@16 (both non-zero), level array at 28..39, band count at byte 41, three 8-bit params at 42/43/44.
  4. Level values map to 1/8/16 bpp (0x6cb7c…0x6cbb0).
  5. Export (slot 21) takes x@16, y@20, w@24, h@28, validated against the resource's width/height 16/20, and allocates its own 200-byte context.
  6. PUT_TILE (slot 19) and EXPORT share the bit-0x28 gate.

Inferred

  1. Op 2 = "create by value" (the image resource); op 1 = the other DEC create variant.
  2. Words 8/9 (bytes 32/36) of the export request carry the destination.
  3. Slot 20 is a photoflo definition; slot 43 is bind/tap.

Unknown

  1. Whether export needs a client-created Pixmap or makes one (inside 0x63432).
  2. The meaning of export words 2, 3 and 10, 11 (bytes 8, 12, 40, 44).
  3. Slot 19's count unit (tiles vs bytes) and whether the payload is packed rows.
  4. Slots 20 and 43 in full.

9. Next probes (for the worker with server access)

id probe distinguishes
P1 create (slot 8, op 2) with word7 = 2, then query 24 on the id if the reply is non-empty (it was empty before) the level array is live and the resource is class 2
P2 after P1, slot 19 with count = 1 and then count = 512 which unit the count uses: one succeeds, the other is rejected/empty
P3 after P1, slot 21 with x=y=0, w=h=64 and words 8..11 zero, then with a pixmap XID from XCreatePixmap(depth 1, 64x64) in word 8 success on one variant names the destination field
P4 create op 1 with word7 = 2, then slot 21 confirms class 3 is not exportable (BadIDChoice at the lookup)
P5 slot 19 on the class-3 id confirms the class-2-only rule

Order: P1 first — it is the single probe that tests the new finding. If P1's reply is still empty, then the level array is not the only gate and the next step is to decode slot 20 (the photoflo definition) and build the pipeline explicitly.


10. Reproduction

  1. program_open(path=…/vt1200_v22.bin, language="tms34010:LE:32:default", read_only=false); the session is a03f3d788b874b1a976a55417ab9f2d3 (the earlier sessions are gone; the project is disposable).
  2. Listings: xie/ghidra-evidence/pipeline/*.txt. Key addresses: create 0x678DA (slot 8 ops 1/2), resource init 0x6CA82, export 0x62D40, put 0x6A76E, dd input 0x6B640.
  3. Re-derive a field: run DisassembleCommand from the address, then read *A9(0xNN) operands (bit offsets; divide by 8 for bytes).