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:
@@ -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.
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user