Files
vt1200/xie/GHIDRA_XIE_IMPORT.md
T
arti d8d54bb990 feat(xie): a client-chosen XIE image is displayed on the VT1200
The XIE path now runs end to end: XieCreateImage -> XieCreatePhoto ->
XiePutTile -> XieExport(photomap -> depth-1 pixmap) -> XCopyArea(pixmap ->
window), and the client's tile appears on screen at the client's own window
position, pixel-exact.

TWO FAULTS BEHIND THE LONG DEAD END

1. Device model (harness/mame/vt1200.cpp, opt-in VT1200_ACCEL=1).  The
   accelerator's output port 0x5600200 returns the pixel stream the firmware
   pushed into the FIFO -- the firmware then stores it into the drawable itself
   (file 0x125528: MOVE *B6,*B1 ; ADDK 0x10,B1).  The FIFO read pointer was
   never reset between transfers, so consecutive exports read stale words and
   the same fill produced 759 then 516 set pixels.  Reset m_fifo_in/out on the
   data-register write; the export is now deterministic.

2. Measurement, not the device.
   - dwt/screenshot.py without --real reads 1024x1024 at a 128-byte row stride,
     but the VR319 default programs 1280x1024 at a 2048-bit (256-byte) pitch.
     That is the "shifted" picture; --real is the correct read.
   - The earlier "0 differing bytes over 128 KiB" VRAM comparison stepped peek
     by 1024 *bit* addresses, so it scanned rows 0..63 only -- never the window
     at rows 200..263.  Over the real raster the white/black diff is 576 bytes
     spanning exactly those rows.
   - New instrument: decclient does a server-side XGetImage readback of the
     pixmap and window and counts set pixels.  That separated "the server never
     drew it" from "the framebuffer view is wrong".

EVIDENCE (reproduced twice, the second time from a fresh machine)

  no export vs 0xFF : 4096 / 4096 pixels differ
  no export vs 0x00 :    0 / 4096   (the window background is black)
  0xFF      vs 0x00 : 4096 / 4096
  0xAA      vs 0x0F : 2048 / 4096   (the two stripe phases)

Tile byte 0xAA reproduces as 1-pixel vertical stripes, 0x0F as 4-pixel, with
no shear -- so the placement is right and the pixels are the client's.

Contents: xie/ holds DEC XIE 2.3 headers, 84 man pages and complete
client+server source; a libXie.a and decclient built from DEC's own code; the
XIE probes; and evidence/ with every measurement, including explicit
retractions of the earlier wrong readings (VRAM_IDENTICAL_FOR_BOTH_FILLS and
friends).  xie/evidence/XIE_IMAGE_DISPLAYED.md is the report; frames are in
xie/evidence/png/.

The accelerator model stays opt-in: it answers the status poll and relays the
pixel stream, but performs no compositing, so it is a stand-in and the verified
default configuration is unchanged (a no-env run still parks in the level-0
diagnostics menu at PC=3FDCE400).
2026-11-01 23:22:25 +02:00

12 KiB
Raw Blame History

XIE 2.3 — why PUT_TILE refuses, and what binding it wants

See GHIDRA_XIE_SLOT20.md: PUT_TILE's payload pointer (request byte 32) is handed to the DD input through a transient band-record byte 5 and cleared; slot 20 runs the per-band DD chain over the resource's 3 bands; the band array (res->0x160 → band+0xd8) is created by the slot-8 create path.

Fourth pass. 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 first — it has the resource class/level model that this pass builds on. Raw listings: xie/ghidra-evidence/import/ (lookup_6B966, slot07_op1_6742C, slot07_op2_6759A, resource_init_6CA82).

Offset convention (important). Offsets in these listings are bit offsets. *A9(0x20) = request byte 4; *A11(0x120) = resource byte 36; *A11(0x140) = resource byte 40.


1. The lookup helper 0x6B966 [C]

Slot 19 calls it with seven arguments. The pushes at 0x6a790…0x6a7ae are, in memory order: req[24], 3, req[4], &local0, caller A13, 0, client, so the C signature is:

f(client, 0, &callerFrameSlot, &local0, id, mode, band)
       ^        ^                 ^          ^     ^      ^
       |        |                 |          |     |      +-- arg6 = request byte 24
       |        |                 |          |     +--------- arg5 = 3 (mode)
       |        |                 |          +--------------- arg4 = request byte 4 (id)
       |        |                 +-------------------------- arg3 = &local
       |        +-------------------------------------------- arg2 = caller frame (out)
       +----------------------------------------------------- arg1 = 0 from slot 19

The body:

rom:0006b97c push *(0x1ad980)            ; resource proc table 1
rom:0006b984 push *(0x1ad990)            ; resource proc table 2
rom:0006b98c push arg4                  ; = request byte 4 = the id
rom:0006b990 CALLA 0x07f174a4           ; lookup(id)  -> 0xD74A4
rom:0006b996 MOVE A8,*A7                ; store result through arg2
rom:0006b99a JRNE 0x0006ba8c            ; found -> class dispatch
rom:0006b99e MOVK 0xe ...               ; NOT FOUND -> error 14 = BadIDChoice

Class dispatch, on resource byte 4 (the class written by create):

rom:0006ba8c MOVE *A7,A11,0x1
rom:0006ba90 MOVE *A11(0x20),A8,0x0     ; A8 = resource byte 4 = class
rom:0006ba94 DEC A8 / JREQ 0x0006b9c4   ; class 1 -> path A
rom:0006ba98 DEC A8 / JREQ 0x0006ba1e   ; class 2 -> path B
rom:0006ba9c DEC A8 / JREQ 0x0006ba1e   ; class 3 -> path B (same code)
rom:0006baa0 JR 0x0006ba66              ; anything else -> BadIDChoice

Path B (class 2 or 3) — the two gates that matter:

rom:0006ba1e MOVE *A13(0xffc0),A11,0x1  ; arg1
rom:0006ba22 JREQ 0x0006ba32            ; slot 19 always passes 0
rom:0006ba32 MOVE *A13(0xffa0),A7,0x1
rom:0006ba36 MOVE *A7,A7,0x1
rom:0006ba38 MOVE *A7(0x140),A7,0x1     ; resource byte 40
rom:0006ba3c JREQ 0x0006baa2            ; byte40 == 0  -> continue   (good)
rom:0006ba3e ... MOVK 0xe               ; byte40 != 0  -> BadIDChoice (14)
rom:0006baa2 MOVE *A13(0xffa0),A11,0x1
rom:0006baa6 MOVE *A11,A7,0x1
rom:0006baa8 MOVE *A13(0xff80),A11,0x1  ; &local0
rom:0006baac MOVE *A7(0x120),*A11(0x0),0x1   ; local0 = resource byte 36
rom:0006bab2 MOVE *A13(0xff20),A7,0x1   ; arg6 = band index (request byte 24)
rom:0006bab6 JRLT 0x0006babc
rom:0006bab8 MOVE *A11,A7,0x1
rom:0006baba JRNE 0x0006baea            ; byte36 != 0 -> band checks
rom:0006babc MOVE *A13(0xff20),A7,0x1
rom:0006bac0 JRLT 0x0006bbce            ; negative band -> done
rom:0006bac6 MOVK 0xa ...               ; byte36 == 0 and band >= 0 -> BadAccess (10)

and the band-mask test that follows (0x6bb1c…0x6bb48):

rom:0006bb22 MOVK 0x1,A7
rom:0006bb24 SLL A11,A7                 ; 1 << band
rom:0006bb2c MOVE *A11(0x60),A11,0x1    ; resource byte 12  (mask B)
rom:0006bb30 AND A11,A7
rom:0006bb32 JRNE 0x0006bb4a            ; bit set in mask B -> BadMatch below
rom:0006bb42 MOVE *A7(0x40),A7,0x1      ; resource byte 8   (mask A)
rom:0006bb46 AND A7,A11
rom:0006bb48 JRNE 0x0006bbe2            ; bit set in mask A -> OK
rom:0006bb4a ... MOVK 0x8 ...           ; BadMatch (8)

Consequences.

observation explanation
class-3 resource → BadIDChoice (14) its byte 40 != 0, so path B rejects it before the band logic
class-2 resource → BadAccess (10) byte 40 == 0 (good) but byte 36 == 0, and the band index was ≥ 0
class 1 → BadIDChoice path A needs arg1 != 0; slot 19 always passes 0

So PUT_TILE wants class 2 (or 3), with byte 40 == 0 and byte 36 != 0, and then the requested band bit must be set in resource byte 8 or byte 12.

Resource byte 8 and byte 12 are exactly the create request's byte 41 and byte 43 (GHIDRA_XIE_PIPELINE.md §2), so the band mask comes from create. Resource byte 36 is not written by the create path I traced — that is the missing piece.

Band index. Slot 19's count/flags word at request byte 24 is not a count: 0x6bab2 feeds it as the band number. The live probe sent count = 1, i.e. band 1, and only band 0 can plausibly be masked in by a create with req[41] = 1 (mask byte 8 = 1 → bit 0).


2. Slot 7: bind, and the fields are swapped [C]

Both sub-handlers take a 4-word request and immediately resolve LookupPhotos(client, 0, &local, ·, ·) with the last two pushed values being request byte 8 and then request byte 12:

rom:0006744e MOVE A14,-*SP,0x1            ; op1, 0x6742C
rom:00067450 MOVE *A9(0x40),*A14+,0x1     ; push request byte 8
rom:00067454 MOVE *A9(0x60),*A14+,0x1     ; push request byte 12
rom:00067458 MOVE A11,*A14+,0x1           ; &local
rom:00067460 MOVE A7,*A14+,0x1            ; 0
rom:00067462 MOVE *A13(0xffe0),*A14+,0x1  ; client
rom:00067466 CALLA 0x07ea6fa0             ; LookupPhotos(client, 0, &local, b12, b8)
rom:00067472 JRNE 0x00067590              ; failure -> returned as-is
rom:00067476 MOVK 0x14 ... MOVK 0x1
rom:00067480 CALLA 0x07f63a30             ; alloc 20 bytes
rom:000674c6 MOVK 0x1,A11
rom:000674c8 MOVB *A11,*A7(0x20)          ; new object byte 4 = **class 1**
rom:000674ce MOVE A11,*A7(0x30),0x0       ; new object bytes 6-7 = 1
rom:000674d2 CALLA 0x07f63b48
rom:000674d8 MOVE A8,*A7(0x80),0x1        ; object bytes 16-19 = result

LookupPhotos' last argument is the type/class, the one before it the id (same convention as slots 11/12/24). Therefore, for slot 7:

byte 8 = resource type/class, byte 12 = resource id. The probe sent [op, id, id2] — i.e. an id where a class belongs — which is exactly why every ordering answered BadIDChoice on the second value: the lookup was searching for id2 of class id, which cannot exist.

op 1 (0x6742C) = allocate a class-1 object (20 bytes) that references the (type, id) pair — a reference binding. op 2 (0x6759A) = same lookup, then walk res->byte9 bands, collect each band record's +0x1a8 into a local array and hand them to the resource-init routine 0x6CA82 — a copy/derive binding.

Matching DEC vocabulary, slot 7 is XIEP_BIND_PHOTOFLO in its two variants (bind by reference / by value). [I] for the name, [C] for the semantics.

Note that op 1's product is class 1, and class 1 is exactly the class that PUT_TILE rejects (arg1 == 0 path). So binding a class-3 photomap into a class-2 sink is not something slot 7 does — it produces class 1.


3. What still blocks PUT_TILE: resource byte 36 [U]

Two candidate explanations, both consistent with the code:

  1. A band must be created on the class-2 resource. Byte 36 looks like the band/strip count of the resource record; the create allocates band records (res+0x160 array, count from res byte 9) but I found no store to byte 36 in the create path. Slot 43 does build a new record through the same 0x6CA82 init routine, using band sub-records (band+0x1a8) collected from an existing resource — so slot 43 ("execute/bind") is the best candidate for materialising bands and setting byte 36.
  2. The band index is simply wrong. count = 1 means band 1; band 0 is the only one a req[41] = 1 create masks in.

Both are testable in two requests, and the first probe below distinguishes them.

Slot 43 in one line (10 words, {id@4, x@8, …}): it resolves the id, iterates the resource's bands, collects band->0x1a8 into a local array, and calls 0x6CA82 to (re)build a resource record with those bands. It succeeded on the class-3 resource with zeros because it only needs a valid id. [I] — it looks like "execute / materialise the pipeline".


4. Exact next probes

Each on one connection: minor 1 (init) → create → probe.

P1 — the band index (1 request). Same create as today, then:

minor 19, len 136
  word1 id        = C0AAAA
  word2 x = 0     word3 y = 0
  word4 w = 64    word5 h = 64
  word6 band      = 0            <-- was 1
  word7 flags     = 0
  + 512 bytes (128 words) of pixels
  • BadMatch (8) → byte 36 is non-zero and only the mask is wrong: fix by setting create req[41] = 1 (mask byte 8 = 1 → band 0) or req[43] (mask byte 12). Then either mask works.
  • BadAccess (10) again → byte 36 really is zero: a band must be created first → go to P2.

P2 — create the band (2 requests). After the create, run slot 43 with the class-2 id and zeros (it already succeeds on class 3):

minor 43, len 10   [id@4 = C0AAAA, rest 0]

then repeat P1. If BadAccess turns into Success or BadMatch, slot 43 is the band-materialising step and must precede every PUT_TILE.

P3 — the bind (optional). If P2 does nothing, try slot 7 with the corrected field order, both op values, using the class-2 resource id as byte 12 and a class code as byte 8:

minor 7, len 4
  word1 op   = 1  (then 2)
  word2 type = 2          <-- class of the target, NOT an id
  word3 id   = C0AAAA

Success means the bind worked; the current failure (BadIDChoice) is then purely the swapped fields.

P4 — fallback import. If PUT_TILE still refuses, slot 20 (35 words) is the remaining import-shaped request; its header is {id@4, …, } with three 0x20-byte element records. That is where an explicit Import element would live, but its layout is still undecoded, so P1→P2 should be exhausted first.


5. Confirmed / Inferred / Unknown

Confirmed

  1. 0x6B966 resolves the id through the resource tables; a miss is BadIDChoice (14).
  2. Class dispatch on resource byte 4: class 1 → path A (needs arg1 != 0, slot 19 passes 0, so always BadIDChoice); classes 2 and 3 → path B.
  3. Path B requires resource byte 40 == 0 (else BadIDChoice) and resource byte 36 != 0 (else BadAccess when the band index is ≥ 0).
  4. The band index is request byte 24, tested as 1 << band against resource byte 12 and byte 8; a mismatch is BadMatch (8).
  5. Resource bytes 8 and 12 are the create request's bytes 41 and 43 (band masks).
  6. Slot 7: byte 8 = type/class, byte 12 = id; op 1 allocates a class-1 object, op 2 rebuilds a record from another resource's bands.

Inferred

  1. Slot 7 is BIND_PHOTOFLO; slot 43 is execute/materialise.
  2. Resource byte 36 is the band count and is set by slot 43, not by create.

Unknown

  1. What writes resource byte 36 (and what clears byte 40 on a class-3 record).
  2. Slot 20's 35-word layout (the explicit Import-element path).
  3. Whether the payload after a successful PUT_TILE must be packed rows.

6. Reproduction

  1. Session a03f3d788b874b1a976a55417ab9f2d3, base rom:00000000.
  2. Listings: xie/ghidra-evidence/import/*.txt (lookup_6B966 = the helper, slot07_op1_6742C, slot07_op2_6759A, resource_init_6CA82).
  3. Re-derive an argument order: count the pushes between MOVE A14,-*SP,0x1 and the CALLA, then map them right-to-left onto A13-0x20, -0x40, ….
  4. Re-derive a field: *A9(0xNN) / *A11(0xNN) are bit offsets; divide by 8.