Files
TeslaRel410/emulator/firmware-decomp/MICROCODE-DECODE-NOTES.md
T
CydandClaude Opus 4.8 0f9ff000c4 Decode plane constants: fixed-point screen coordinates
edge_decode.py: the edge payloads' non-float words carry the plane constant/
vertex terms as .8 fixed-point screen coordinates (0x0000ec00 = 60416 = 236.0,
a screen-x in the object's range), not IEEE floats -- which is why they weren't
in the float list. Locates the last missing piece for edge reconstruction:
A/B slope (float, verified 0.2%) + C (fixed-point coord). Readout §02 notes it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 16:39:25 -05:00

9.3 KiB

PXPL5 IGC micro-code — decode notes (session 6j, 2026-07-16)

Working notes toward executing the compiled IGC micro-code so the ground/sky (which carry no stored VSTRIP vertices) can be rendered. Complements igc_array.py (the array's computational model) — this is about the actual compiled stream the DMA ships.

The per-region DMA command list is decoded (from real capture)

0xf0411cd4 (fst.d) copies each screen region's DMA command list into its queue page. Captured from the cap7 death-cam draw (scratchpad/coefdump.py), one region's list (@ 0x0801fa40) decodes cleanly against DMAENGN.H as {addr, opcode} 64-bit pairs (opcode = top nibble, low 7 bits = word count):

0x08015000  SEND(4)       ; edge coefficients
0x00000000  FLUSH
0x08015020  SENDE(0x45)   ; z / colour  (69 words)
0x00000000  FLUSH
0x08014100  TXDN
0x08015260  SEND(0x21)    ; 33 words
0x08015380  SEND(0x29)    ; 41 words
0x00000000  TILE(id)      ; per-region tile id in the addr slot (0x20/0x40/0x60/…)
0x0801f008  GOTO          ; link to the next region's queue
0x08015000  FLUSH
0x08015000  SEND(0x10)
0x08015000  FLUSH

Key: every region's list references the same payload addresses (0x08015000, 0x08015020, …) and differs only in the TILE(id) slot and the GOTO link. The geometry micro-code is tile-relative and broadcast to every tile the primitive covers — the array evaluates it at each tile's own origin. So this whole list is one primitive across many tiles; other primitives (terrain, sky) have their own DMA lists pointing at their own payloads.

The SEND payloads carry embedded FLOAT coefficients

Dumped the payloads (scratchpad/payload_dump.py). They are not opaque — they interleave control words with recognisable IEEE-754 floats = the edge / plane / colour coefficients:

SEND(4)  @0x08015000 :  00000100  3e013991(=0.1262)  0000ec00  0000…      ; edge
SENDE    @0x08015020 :  00000100  3a804834(=9.79e-4)  8401213a  00000021  ; z/col
                        ba01253a(=-4.93e-4) 00143a21 8381213a 00000022    ; per bit-plane,
                        ba01253a(=-4.93e-4) 00133a22 8301213a 00000023    ; addr 0x21,0x22,…
                        …  (a bit-serial MEMpluseqMEM sweep: the float is the
                            increment, the control words carry the target
                            bit-plane address + length)
SEND(0x21) @0x08015260:  floats 0.1253, 9.79e-4, 0.1262, 0.00111, -0.0157, -0.0078 …
SEND(0x29) @0x08015380:  floats -0.00196, -0.0627 (repeated per bit-plane) …

Interpretation: 00000100 recurs as an instruction header; each coefficient load is {header, float, control(addr/len), addr}. The repeated float with an incrementing address (0x21,0x22,0x23,…) is the bit-serial plane interpolation (IGCOPS.C MEMpluseqMEM) sweeping the bit-planes of a z/colour value, the float being the Ax+By+C increment.

The SENDE sweep has a regular 4-word instruction stride

After the 00000100 header + a base float, the z/colour SENDE settles into a clean 4-word instruction (scratchpad analysis):

word0  increment float   (e.g. -4.93e-4, constant across the sweep)
word1  00 LL 3a AA        ; LL = a length/countdown (0x14,0x13,0x12,… decrementing)
                          ;      AA = a bit-plane address (0x21,0x22,0x23,… incrementing)
word2  8H 01 21 3a        ; H high-nibble drifts down (0x84,0x83,0x82,…) -> op/plane sel
word3  00 00 00 NN        ; NN = destination bit-plane (0x21,0x22,…)

i.e. a MEMpluseqMEM sweep: add the increment to each successive bit-plane of the z (or colour) word, LL bit-planes long. The float is the Ax+By+C plane increment; the control words carry the target bit-address + length. So a plane value is reconstructable as {base float, per-x/per-y increment floats, bit window} once the control-word field split is pinned.

The coefficient VALUE encoding is decoded: bit-serial place value

Grouping the payload floats (scratchpad/decode_corr.py, chain_decode.py) shows they are not independent — they fall into clean x2 doubling chains:

0.00788  0.01576  0.03153  0.06305  0.1261  0.25221  0.50441  1.00883   (x2 each)
0.00783  0.01566  0.03132  0.06265  0.12527                             (a 2nd chain)
0.00049  0.00098  0.00196 …                                             (a 3rd)

That is exactly how a bit-serial adder holds a number: bit-plane k carries the coefficient x 2^k. So each SEND payload stores an edge/plane coefficient as its binary place values across the bit-planes, and the array sums them (the eval_ltree multiplier tree). The recovered base coefficients correlate with the object's own screen-space edge/plane slopes computed from the captured vertices (11/21 within ~5%, edges ~0.125 vs geometry edge-normals ~0.13). Fixed-point scales are in FOOTER.SS: .Czscale = 0x497fffff = 2^20 (z), .Ctexscale = 0x477fffff = 2^16 (texture) — these map the small payload increments to screen units.

Consequence: igc_array.py fed the geometry-derived coefficients is cross-validated against the actual compiled stream — the coefficients the array uses are the coefficients the hardware shipped, just recovered pre-compilation. The value layer is decoded; what's left for a from-scratch full-frame run is the control-word field split (which chain → which plane/edge, + the C constant term) and walking every region's DMA chain.

SENDE control-word split (z/colour sweep) — decoded

Full SENDE dump (scratchpad/ctrl_split.py) shows a perfectly regular 4-word instruction, repeated once per bit-plane:

word0  FLOAT      constant accumulator increment (e.g. -4.9265e-4), same every instr
word1  00 LL 3a AA   LL = length countdown (0x14,0x13,…,0x00); AA = source bit-addr
                     (0x21,0x22,…); 0x3a = op field
word2  <place-value> the coefficient's value at this bit-plane -> the x2 chain
                     (…8401213a,8381213a for the tiny low planes; then bf81213a=-1.009,
                      bf01213a=-0.504, be81213a=-0.252, … bc01213a=-0.00788)
word3  00 00 00 NN   destination bit-plane (0x21..0x31 => a 17-bit fixed-point value)

So SENDE writes one z-or-colour value bit-serially: 17 bit-planes (0x21..0x31), each carrying coeff * 2^k (word2), with a constant carry/increment (word0) and a length countdown (word1 LL) — a MEMpluseqMEM accumulation. The 0x3a/0x21 fields in words 1&2 are the opcode + operand selector (the exact bit split of 3a/21/81-vs-01 still to be pinned against IGCOPS.C op numbers, but the operand roles are clear).

SEND(0x21)/SEND(0x29) (edge/setup) differ: they carry 00000100 headers interspersed with 3-word [float, ctrl, ctrl] groups (e.g. 0.125275, 0.126196, 0.00110586 = the edge/plane bases) — same value encoding, different framing.

To render z from a SENDE from scratch: read word2 across the 17 planes to reconstruct the coefficient (or just take the recognisable float planes), apply .Czscale=2^20, that is the per-pixel z increment; the base/C term is the plane's value at the tile origin (still to be located in the stream). Then it is a plain igc_array.py z-plane.

Plane constants are fixed-point screen coordinates

The edge payloads' non-float words carry the plane's constant/vertex terms as fixed-point screen coordinates (scratchpad/edge_decode.py):

SEND(4) edge @0x08015000:  00000100  3e013991(A=0.1262)  0000ec00  00000000
                                                          ^ 0xec00 = 60416 = 236.0 * 256
SEND(0x21) @0x08015260:    … 0000ec00 (=236.0)  0000b53a (=181.2) …

0x0000ec00 / 256 = 236.0 = a screen-x right in the object's x[126,255] range (first captured vertex was x≈237.5). So the constant term C is stored as a .8 fixed-point coordinate (word/256), not an IEEE float — which is why it wasn't in the float list. SEND(0x29) is a long single-coefficient sweep (-0.0626 repeated ~30x across bit-planes = the coarse interpolant). This locates the last missing piece for edge reconstruction: A/B slope (float, verified 0.2%) + C (fixed-point coord). Remaining is pairing them per triangle + all-regions assembly.

What this changes

The micro-code decode is now extraction + bit-serial execution, not blind ISA reversing:

  1. parse the payload into {op-header, float, bit-addr, len} instructions,
  2. map each float to its plane role (edge A/B/C, z, r/g/b) by position,
  3. drive igc_array.py's pixel-memory with the real coefficients per tile (the array already does eval_ltree + z-buffer + readout).

Blocker to a clean full decode: igc_opco.h (the opcode encoding header, \projects\dbi0150\dbi0151\ucode\igc_opco.h) is not in the dump — the 00000100 / 0x3a.. control-word field layout has to be reversed from these examples + IGCOPS.C op semantics + the emit sites in EOF.S/PXPL5OK.SS.

Next session

  • Reverse the control-word layout (header 00000100; the ..3a / 0x21 fields = op + bit-address + length) from the SENDE sweep (cleanest, most regular).
  • Extract the object's edge+z+colour floats, feed igc_array.py, confirm it reproduces the object from the real coefficients (not the geometry-derived ones).
  • Then walk every region's DMA list, run all payloads tile-by-tile → full frame.
  • Tools: scratchpad/coefdump.py (DMA lists), scratchpad/payload_dump.py (payload floats). Restore from scratchpad/snapv2.pkl (cmd 735 death-cam).