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>
107 lines
3.9 KiB
Python
107 lines
3.9 KiB
Python
"""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")
|