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

13 KiB
Raw Permalink Blame History

XIE 2.3 — slot 20, the band array, and the PUT_TILE payload hand-off

Program vt1200_v22.bin, Ghidra session a03f3d788b874b1a976a55417ab9f2d3. Addresses are file offsets; mame_bit = 0x3F200000 + off*8; CALLA operand − 0x7E40000 = file offset. Read GHIDRA_XIE_PIPELINE.md and GHIDRA_XIE_IMPORT.md first.

Raw listings: xie/ghidra-evidence/slot20/.

Offset convention — as in the earlier reports, every offset in the disassembly is a bit offset. *A9(0x20) = request byte 4; *A5(0x28) = byte 5 of A5; *A7(0x120) = byte 36; *A7(0x140) = byte 40. This distinction matters for one of the two claims in the brief (see §3).


1. Correction: the +0x28 write is inside slot 19, not slot 20

The instruction the brief points at,

rom:0006a98e MOVE *A13(0x120),*A5(0x28),0x1

lies in slot 19's body (0x6A76E … 0x6AA54), not slot 20's (0x6AA54 …). Read with the bit-offset convention it is:

  • A5 = one band/element record (A5 = *(A7 + 0x160) one instruction earlier, 0x6a98a),
  • *A13(0x120) = frame local 0x120, which was filled at 0x6a882 with A5 = A9 + 0x100, i.e. a pointer to request byte 32,
  • *A5(0x28) = byte 5 of that band record (bits, not byte 0x28).

So the sequence at the end of PUT_TILE is:

rom:0006a87c MOVE A9,A5
rom:0006a87e ADDI 0x100,A5            ; A5 = request + 0x100 bits = request byte 32
rom:0006a882 MOVE A5,*A13(0x120),0x1  ; local = pointer to the pixel payload
...
rom:0006a98a MOVE *A5(0x160),A5,0x1   ; A5 = resource->band[]
rom:0006a98e MOVE *A13(0x120),*A5(0x28),0x1  ; band.byte5 = payload pointer
rom:0006a9a2 CALLA 0x07eab640        ; 0x6B640  device-dependent input
rom:0006a9e2 SUB A7,A7
rom:0006a9e4 MOVE A7,*A5(0x28),0x1    ; band.byte5 = 0   (cleared again)

This is the payload hand-off, and it is the answer to "where do the pixels come from": PUT_TILE passes the request buffer itself (from byte 32) down to the DD input routine through a scratch field, and clears the field immediately afterwards. It is transient, per call — nothing persists.

It also fixes an open question from pass 3: the PUT_TILE payload starts at request byte 32, exactly the word after the 8-word header ({hdr, id, x, y, w, h, band, flags}).


2. Slot 20 — request layout and behaviour [partial, C for the structure]

Entry 0x6AA54.

rom:0006aa5e MOVK 0x19,A11 / MOVE A11,*A13(0xc0),0x1   ; default 25 (see below)
rom:0006aa6e MOVE *A9(0x10),A7,0x0                     ; stuff->length
rom:0006aa72 CMPI 0x23,A7                              ; must be exactly 35 words
rom:0006aa7a MOVK 0x10 -> BadLength
rom:0006aa7e CALLA 0x07f67f38                          ; context/undo mark
rom:0006aa84 MOVE A8,@0x1b2b30,0x1
rom:0006aa8c MOVI 0xffff,A7 / push                     ; arg = 0xFFFF (class "any")
rom:0006aa94 MOVE *A9(0x68),A8,0x0                     ; arg = request byte 13 (8-bit)
rom:0006aa9a MOVE *A9(0x20),*A14+,0x1                  ; arg = request byte 4  (id)
rom:0006aaa4 push &local(0x40)
rom:0006aaaa push &local(0)
rom:0006aaac push A13
rom:0006aaae push client
rom:0006aab2 CALLA 0x07eab966                          ; LOOKUP 0x6B966 (see pass 4)
rom:0006aabe JRNE 0x0006afdc                           ; error -> return it

Then a loop over the resource's bands:

rom:0006aad8 MOVE *A7(0x160),A5,0x1        ; A5 = resource->band[] (byte 44)
rom:0006aae4 MOVE *A5(0xd8),A7,0x1          ; band record (byte 27)
rom:0006aae8 JREQ 0x0006ab46                ; no record -> result 0
rom:0006aaf6..0x6ab0e  A7 = band->0x1a8 (bit) ; per-band device object
rom:0006ab0e CALLA 0x07fc22de               ; 0x1822DE \
rom:0006ab14 CALLA 0x07fc2c34               ; 0x182C34  |  device-dependent
rom:0006ab2a CALLA 0x07fc2c34               ; 0x182C34  |  band setup chain
rom:0006ab30 CALLA 0x07fc1aca               ; 0x181ACA  |
rom:0006ab36 CALLA 0x07fc288a               ; 0x18288A  |
rom:0006ab3c CALLA 0x07fc216a               ; 0x18216A /
rom:0006ab48 MOVE A7,*A11(0x120),0x1        ; per-band result -> frame array
rom:0006ab6a MOVE *A13(0x80),A7,0x1         ; i
rom:0006ab6e SUBK 0x3,A7
rom:0006ab70 JRC 0x0006aacc                 ; loop while i < 3

i.e. three bands, xieValMaxBands = 3, each with a device-dependent record; results are summed into frame local 0x100.

After the loop the handler validates request bytes against the per-band state and returns BadMatch (8) on mismatch:

rom:0006ab82 MOVE *A13(0x40),A11,0x1
rom:0006ab88 MOVE *A11(0x8),A7,0x0     ; band attribute (16-bit at byte 1)
rom:0006ab8c MOVE *A9(0x70),A5,0x0     ; request byte 14
rom:0006ab92 JRNE  -> BadMatch
rom:0006ab94 MOVE *A11(0x10),A7,0x0    ; band attribute at byte 2
rom:0006ab98 MOVE *A9(0x60),A5,0x0     ; request byte 12
rom:0006aba0 MOVE *A11(0x20),A7,0x1    ; 32-bit attribute at byte 4
rom:0006aba4 MOVE *A9(0x40),A5,0x1     ; request byte 8
rom:0006abac MOVE *A11(0x40),A7,0x1    ; attribute at byte 8
rom:0006abb0 MOVE *A9(0x80),A5,0x1     ; request byte 16
rom:0006abc4 MOVE *A11(0x80),A7,0x1
rom:0006abce JRNC 0x0006abf6
rom:0006abd0 ... MOVK 0x8 -> BadMatch
word byte meaning (as far as the code shows)
0 0 header; length must be 0x23 = 35
1 4 resource id (fed to 0x6B966 with class 0xFFFF)
2 8 32-bit attribute compared with band+4
3 12 16-bit attribute compared with band+2
— 13 8-bit mode passed to the lookup (0x6aa94); also drives the attribute checks
— 14 16-bit attribute compared with band+1
4 16 32-bit attribute compared with band+8
5 20 not read in the traced path
6 24 not read in the traced path
7 28 not read in the traced path
8…34 32…139 not read in the traced path

MOVK 0x19 (25) at 0x6aa5e seeds frame local 0xc0 — it is not a request field: the local is overwritten by the mode-dependent branches at 0x6ac08, 0x6ac10, 0x6ac1e later in the handler. It is a default for a computed value, so the "25-element notion" in the pass-3 note is not an element count.

Slot 20 is therefore a per-band operation on an existing resource — the same device-dependent band chain that the create path runs — not a photoflo definition, and not the thing that creates the band array.


3. Where the band array comes from [C]

The band array is created by the create request (slot 8 op 1/op 2 → 0x678DA → 0x6CA82), not by slot 20 or slot 43:

rom:0006cb54 MOVE *A9(0x48),A11,0x0    ; band count = resource byte 9 (1 or 3)
rom:0006cb58 CMP A11,A7 / JRNC ...
rom:0006cb64 MOVE *A13(0xffc0),A9,0x1  ; the level array handed in by the caller
rom:0006cb6a MOVE *A11,A11,0x1         ; level entry
...
rom:0006cc0e MOVE *A7(0x160),A9,0x1    ; resource->band[]   (byte 44)
rom:0006cc12 SLL 0x5,A7                ; i * 4 bytes
rom:0006cc18 ADD  A7,A9                ; &band[i]
rom:0006cc48 CALLA 0x07eac660          ; 0x6C660  allocate the band record
rom:0006cc4e MOVE A8,*A9(0xd8),0x1     ; band[i] + 0xd8 (byte 27) = record

and the record allocator itself:

rom:0006c660 MMTM SP,0x154
rom:0006c66a MOVI 0x39,A11             ; 57-byte record
rom:0006c670 CALLA 0x07f639f4          ; allocate
rom:0006c692 MOVK 0x8,A9  / *A11,0x0   ; class -> 8 bpp     (0x16-bit field)
rom:0006c69e MOVK 0x10,A9 / *A11,0x0   ; class -> 16 bpp
rom:0006c6a8 MOVK 0x0,A9  / *A11,0x0   ; class -> 0
rom:0006c6b0 ... CALLA 0x07fc22de … 0x07fc216a   ; same DD chain as slot 20

So: create → per-band record (0x39 bytes) → stored at resource->band[i] + 0xd8, one record per band, band count = the create request's byte 41 (3 → three bands, anything else → one). That is why a create with req[41] = 1 and a non-zero level entry gives a resource with one populated band and mask bit 0 set in resource byte 8.

Resource byte 36 — still not written anywhere I can see

0x6BAAC copies resource[bit 0x120] (byte 36, 32-bit) into a local and requires it to be non-zero (GHIDRA_XIE_IMPORT.md §1). A scan of the whole resource module (0x65000 … 0x6D000) for stores to bit 0x120 / 0x140 on a non-frame base yields exactly one hit:

rom:0006ce56 MOVE A11,*A9(0x140),0x1     ; *A9(0x140) = 0  (clears byte 40)

That is inside the helper slot 20 calls at 0x6CD68, and it clears byte 40 of an object reached through resource->band[] — consistent with PUT_TILE requiring byte 40 == 0 and with slot 20 "re-arming" bands.

No store to byte 36 exists in that whole range. Either it is written outside the resource module, or it is part of a block copied in by the create path's pre-call helper 0x1238C2 (which receives &request[20]). This is the one remaining structural gap.


4. Ranked next probes

The static model cannot finish this; these are ordered so each result eliminates a branch. All assume the class-2 resource that already works for slot 21.

P1 — PUT_TILE with band 0 (1 request, highest value). minor 19, len 136: id, x=0, y=0, w=64, h=64, **band=0**, flags=0 + 512 bytes.

  • Success → done: the band exists, the payload starts at byte 32 (proved in §1), and only the earlier band=1 was wrong.
  • BadMatch (8) → byte 36 is non-zero and the band mask is what fails: set create req[41] = 1 so resource byte 8 = 1 (1 << 0).
  • BadAccess (10) → byte 36 is zero: the resource has no live band pointer → P2.

P2 — slot 20 as the band arming step (1 request). minor 20, len 35: id@4, byte13 = 1, all other words 0. Expect either Success (bands armed; repeat P1) or BadMatch naming an attribute in bytes 8/12/14/16 — in which case copy the corresponding per-band values from the create parameters and retry. This is the request that runs the same DD band chain (0x1822DE … 0x18216A) that 0x6C660 runs at create time.

P3 — dump the live resource record (0 requests; use the slot-24 query and the existing probe harness). Query 24 on the class-2 id and record which bytes of the 32-byte reply are non-zero. If the reply is empty (as it was before), then instead read the record from the server side is not possible — in that case skip to P4.

P4 — slot 43 (10 words) then P1. minor 43, len 10, [id, 0…] clears byte 40 via 0x6CE56 (proved above) and rebuilds a record from the existing bands; then P1.

P5 — the fallback define path: slot 7 with the corrected field order, minor 7, len 4, [op=1, type=2, id] (byte 8 is the type, byte 12 the id). This allocates a class-1 object (a photoflo/handle) — only useful if P1–P4 all fail, since PUT_TILE rejects class 1 outright.


5. Minimal sequence (best current candidate)

[1] minor 1  len 2   02 00 03 00                                  [C]
[2] minor 8  len 12  op=2, id, w=64@12, h=64@16, req[20]=0,
                     level[0]=2 @28, byte41=01 (one band), rest 0  [C]
[3] minor 20 len 35  id@4, byte13=1, rest 0            (arm bands)  [I]
[4] minor 19 len 136 id, x=0, y=0, w=64, h=64, band=0, flags=0,
                     + 512 bytes of 1-bit pixels                   [I]
[5] minor 21 len 16  id, x=0@16, y=0@20, w=64@24, h=64@28, …       [C]
[6] XCopyArea(pixmap|window)  or the export already drew into it   [C]

Step [3] is the only speculative one, and P1/P2 decide whether it is needed.


6. Confirmed / Inferred / Unknown

Confirmed

  1. 0x6A98E is inside slot 19 (PUT_TILE), and it stores request byte 32 (the payload pointer) into byte 5 of a band record, calls 0x6B640, then zeroes it — a transient, per-call hand-off.
  2. PUT_TILE's payload therefore begins at request byte 32; the 8-word header is {hdr, id, x, y, w, h, band, flags}.
  3. Slot 20 requires length == 0x23, takes the resource id at byte 4 plus a mode byte at 13, loops over the resource's three bands, runs the DD chain 0x1822DE/0x182C34/0x181ACA/0x18288A/0x18216A per band, and validates request bytes 8/12/14/16 against per-band attributes (BadMatch on mismatch).
  4. The band array (resource->band[], byte 44) and its per-band records (band+0xd8, 0x39 bytes) are created by the slot-8 create path (0x6CA82 → 0x6C660), one record per band, count from req[41].
  5. 0x6CE56 clears byte 40 of a band-reachable object; it is the only byte-40/byte-36 store in 0x65000–0x6D000.

Inferred

  1. Slot 20 is "arm/execute the existing bands", not a photoflo definition.
  2. MOVK 0x19 is a computed default, not a 25-element count.
  3. PUT_TILE with band = 0 plus an armed band should succeed.

Unknown

  1. Who writes resource byte 36 — no store found in the resource module.
  2. The meaning of slot 20's words 20, 24, 28 and 8…34.
  3. Whether slot 20 or slot 43 is the arming request (both touch band state).

7. Reproduction

  1. Session a03f3d788b874b1a976a55417ab9f2d3, base rom:00000000.

  2. Listings: xie/ghidra-evidence/slot20/ (slot20_6AA54.txt, band_alloc_6C660.txt, slot20_helper_6CD68_tail.txt, slot19_payload_handoff.txt).

  3. Scan used for the byte-36/40 question:

    # disassemble 0x65000..0x6D000, then keep instructions matching
    #   MOVE ...,*A?(0x120|0x140),0x1   where the base is not A13/A14
    

    which yields only 0x6CE56.