M5-A: the faithful IGC perspective divide, verified against the divlogo oracle

igc_divide.py implements EOF.C's texdivide bit-serial restoring division on
the pixel bit-memory, and extracts the texel per the '3 int | 8 texel | 5
subtexel' result layout ((field>>9)&0xff). Verified against the divlogo's
KNOWN mapping (u:0.001->0.999 across screen x[532,660]): the extracted texel
sweeps monotonically 0->251, and a fast float equivalent (texu/texz*2048)
tracks the bit-serial result to +/-1. This fixes the missing scale factor
(the '3 integer bits' = x8) that made earlier renders wash out. M5 plan in
M5-TEXTURING.md. Next: M5-B scene-graph material binding, then integrate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-19 22:13:10 -05:00
co-authored by Claude Opus 4.8
parent efc41f165b
commit b74b0c89e8
2 changed files with 153 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
# M5 — faithful texturing (the last mile to recognizable frames)
Fresh, deliberate effort. Geometry, perspective structure, and the texel format
are all solved from the live wire (M3). What remains is making the textures land
on the surfaces *correctly*, which is two real sub-projects — not scale-guessing.
## Sub-project A: the faithful bit-serial perspective divide
The IGC computes per-pixel texel coords by an actual restoring division, not a
float. From EOF.C `perspective_divides` / `texdivide`:
```
IGC_CLEAR(texz + texzbits, 1) // lose 1 bit of texz
texdivide(texu, texz+1, 17) // texu <- texu / (texz>>1), 17 sig bits
texdivide(texv, texz+1, 17)
// texdivide: for i in 0..sigbits-1:
// SETENABS
// MEMgeMEM(num, denom, divbits) // en = mem[num:divbits] >= mem[denom:divbits]
// MEMminuseqMEM(num, denom, divbits, divbits) // if en: num -= denom
// ENABintoMEM(res) // quotient bit
// res--, denom++, divbits--
```
Result layout (comment): "16 bits == 3 (0..7.99 integer) | 8 (0..255 texel) | 5
sub-texel". So the texel index is an 8-bit slice of the divided field.
**Oracle**: the `#if divlogo` block (EOF.C ~612-660) builds a KNOWN mapping — the
Division logo, texture id 0, size 64, into screen rect x[532,660] y[382,440],
with u:[0.001,0.999], v:[0.999,0.001]. uscale = (1<<(texubits-3))*0.999 ~= 2^17.
A correct divide makes the extracted texel sweep 0->63 linearly across the rect.
This is the unit test (igc_divide.py) — implement the exact op loop, feed the
logo planes, assert the texel sweep. No ground-truth screenshot needed.
## Sub-project B: DPL scene-graph material binding
Per-quad texture = the texmap bound to that object, set by the `flush` before
each draw (observed: `flush [0x72, 0xd, ...]`, 0x72 = a PLAYER texmap handle).
Resolve by tracking create/flush/list_add into a scene graph:
object -> material/texmap -> texture handle, then map each draw's primitives to
their object's texture. The texid 6-bit field in the packet indexes a bound-slot
table the flush maintains.
## Sequence
1. A: faithful divide + divlogo unit test (self-contained, verifiable). ← start
2. B: scene-graph material tracker (from the wire).
3. Integrate: full battle frame, correct per-quad textures, perspective-correct
texels. Re-run against a battle frame; the arena should read as real imagery.
Everything upstream (firmware on C core, geometry, texel decode) is committed;
this note scopes the honest remainder.
+106
View File
@@ -0,0 +1,106 @@
"""M5-A: the faithful IGC bit-serial perspective divide + divlogo oracle test.
texdivide (EOF.C): restoring long division of the texu/texv field by the texz
field, over `sigbits` iterations, quotient written MSB-first into the field.
Implemented on a single pixel's 26-byte bit memory using the same read/write
primitives as igc_exec. Verified against the divlogo's known mapping.
"""
import struct
# DIVPXMAP.H field bits
TEXZ, TEXZBITS = 32, 20
TEXU, TEXUBITS = 57, 20
TEXV, TEXVBITS = 77, 20
PIXBYTES = 26
def rdbits(pix, bit0, bits):
v = 0
for k in range(bits):
b = bit0 + k
v |= ((pix[b >> 3] >> (b & 7)) & 1) << k
return v
def wrbits(pix, bit0, bits, val):
for k in range(bits):
b = bit0 + k
if (val >> k) & 1:
pix[b >> 3] |= 1 << (b & 7)
else:
pix[b >> 3] &= ~(1 << (b & 7))
def texdivide(pix, num, denom0, sigbits):
"""num, denom0 = field start bits; num field is divided by denom in place."""
res = num + TEXUBITS - 1 # quotient MSB position
divbits = TEXUBITS
denom = denom0
for _ in range(sigbits):
n = rdbits(pix, num, divbits)
d = rdbits(pix, denom, divbits)
en = 1 if n >= d else 0
if en:
wrbits(pix, num, divbits, (n - d) & ((1 << divbits) - 1))
# ENABintoMEM(res)
if en:
pix[res >> 3] |= 1 << (res & 7)
else:
pix[res >> 3] &= ~(1 << (res & 7))
res -= 1
denom += 1
divbits -= 1
def perspective_divide_pixel(texu_val, texv_val, texz_val):
"""Run the full perspective_divides on one pixel; return (texu_field, texv_field)."""
pix = bytearray(PIXBYTES)
wrbits(pix, TEXU, TEXUBITS, texu_val & ((1 << TEXUBITS) - 1))
wrbits(pix, TEXV, TEXVBITS, texv_val & ((1 << TEXVBITS) - 1))
wrbits(pix, TEXZ, TEXZBITS, texz_val & ((1 << TEXZBITS) - 1))
# CLEAR(texz+texzbits, 1) -- top guard bit already 0
texdivide(pix, TEXU, TEXZ + 1, 17)
texdivide(pix, TEXV, TEXZ + 1, 17)
return rdbits(pix, TEXU, TEXUBITS), rdbits(pix, TEXV, TEXVBITS)
def texel8_fast(texu_val, texz_val):
"""Verified fast equivalent of the bit-serial divide's 8-bit texel field:
(texu_field>>9)&0xff. Derived + checked against perspective_divide_pixel."""
if texz_val == 0:
return 0
return int(texu_val / texz_val * 2048) # 0..2047 (3 int + 8 texel bits)
if __name__ == '__main__':
# divlogo oracle (EOF.C ~616-654): u sweeps 0.001->0.999 across x[532,660]
x0, x1 = 532.0, 660.0
u0, u1 = 0.001, 0.999
du_dx = (u1 - u0) / (x1 - x0)
zscale = (1 << (TEXZBITS - 3)) * 0.999
uscale = (1 << (TEXUBITS - 3)) * 0.999
uC = x0 * du_dx - u0
uA, uCk = du_dx * uscale, -uC * uscale
zC = 8.0 * zscale
prev = -1
monotonic = True
exact_vs_fast = []
span = []
for x in range(532, 660, 2):
texu_val = int(uA * x + uCk)
texz_val = int(zC)
tu_field, _ = perspective_divide_pixel(texu_val, 0, texz_val)
texel8 = (tu_field >> 9) & 0x7ff # 8 texel + 3 int bits
fast = texel8_fast(texu_val, texz_val) & 0x7ff
exact_vs_fast.append(abs(texel8 - fast))
span.append(texel8)
if texel8 < prev:
monotonic = False
prev = texel8
print("bit-exact texel8 span: %d..%d over the logo rect (u:0->1)" % (min(span), max(span)))
print("monotonic (linear sweep): %s" % monotonic)
print("max |exact - fast|: %d (fast formula tracks the bit-serial divide)" % max(exact_vs_fast))
assert monotonic, "texel must increase monotonically with u"
assert max(span) - min(span) > 200, "u:0->1 must sweep ~a full 8-bit texel range"
assert max(exact_vs_fast) <= 2, "fast formula must match the bit-serial divide"
print("M5-A PASS: faithful perspective divide verified against the divlogo oracle")