re: the decoder's timers are a double-tap latch on LB/LT, not key repeat
Answers H1's first half with a negative, and decodes what the timing constants are for. C_PAD_DECODER's +0xB4/+0xB8 and +0xBC/+0xC0 (10 and 90, twice) are not a key-repeat delay/interval pair. They are two identical channels of a DOUBLE-TAP detector, on cfg +0x74 (LT) and +0x70 (LB), firing output bits 0x20 and 0x40. The mechanism, from the image: both timers tick down once per update; a press arms the short timer to 10; a RELEASE adds 1000 to it as a flag stored inside the counter, which is why the tick watches for exactly 1000 and clears both there; a second press while the flag is set arms the long timer to 90; and while the long timer runs the output bit is asserted on EVERY update, with the short timer re-armed to 1010 each time. So it is a latch, not a repeat: a double-tap opens a 90-update window and the bit is held for its whole duration. That is a dash or barrel-roll shape, which fits the buttons it is wired to. H1: the directions have NO timer. The D-pad and left stick reach the output word through bare mask tests -- the four left-stick literals at 0x8220C458, C474, C490, C4AC, and DPAD DOWN through cfg +0xA4 -- with no counter loaded, decremented or tested on any of those paths. On the evidence of this layer a held direction does not repeat. Reach: one layer. A menu could implement repeat on top of a held bit, and this says nothing about that -- but it does establish the repeat is not in the shared decoder, so it would have to be per-screen. "One step per deflection" stays authored for the port; this narrows rather than settles. Refutation of my own earlier note that a decoder "is where a game normally puts its repeat timing, its edge detection and its button remap": remap survives, edge detection survives, repeat timing is REFUTED. That clause was a prior about how games are written, not a reading of this one. Also commits tools/ppc-dis, the minimal PowerPC disassembler this corpus has been rebuilding in scratch and losing to container restarts three times in one session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
This commit is contained in:
115
tools/ppc-dis
Executable file
115
tools/ppc-dis
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal PowerPC disassembler for /image/sylpheed.pe.
|
||||
|
||||
tools/ppc-dis <start_va> <end_va> disassemble a range
|
||||
tools/ppc-dis --find-imm <value> [lo hi] every instruction with that immediate
|
||||
|
||||
The image is a FLAT VA DUMP: file offset = VA - 0x82000000. There is no `duckdb`
|
||||
and no PowerPC `objdump` in this container, so this is the working route to the
|
||||
bytes -- and the bytes are primary, the database is somebody's analysis of them.
|
||||
|
||||
⚠️ It lives here rather than in /tmp because the scratchpad is wiped by container
|
||||
restarts, which cost this tool three times in one session.
|
||||
|
||||
Covers the integer/branch/load-store forms this corpus actually reads. Floating
|
||||
point is printed as `FP<op> ... xo=<n>` rather than decoded: no finding so far has
|
||||
needed an FP mnemonic, and a wrong one would be worse than an honest placeholder.
|
||||
"""
|
||||
import struct, sys
|
||||
BASE = 0x82000000
|
||||
IMG = "/image/sylpheed.pe"
|
||||
|
||||
def _mask(mb, me):
|
||||
m, i = 0, mb
|
||||
while True:
|
||||
m |= 1 << (31 - i)
|
||||
if i == me:
|
||||
break
|
||||
i = (i + 1) & 31
|
||||
return m
|
||||
|
||||
XO = {0:'cmpw',32:'cmplw',28:'and',444:'or',316:'xor',60:'andc',266:'add',40:'subf',
|
||||
104:'neg',24:'slw',536:'srw',824:'srawi',23:'lwzx',87:'lbzx',279:'lhzx',
|
||||
151:'stwx',339:'mfspr',467:'mtspr',235:'mullw',491:'divw',459:'divwu',
|
||||
922:'extsh',954:'extsb',407:'sthx',215:'stbx',343:'lhax',55:'lwzux',
|
||||
20:'lwarx',150:'stwcx.',412:'orc',476:'nand',124:'nor',8:'subfc',10:'addc',
|
||||
138:'adde',26:'cntlzw',792:'sraw'}
|
||||
DF = {32:'lwz',33:'lwzu',34:'lbz',35:'lbzu',36:'stw',37:'stwu',38:'stb',39:'stbu',
|
||||
40:'lhz',41:'lhzu',42:'lha',44:'sth',45:'sthu',14:'addi',15:'addis',
|
||||
12:'addic',13:'addic.',7:'mulli',8:'subfic',24:'ori',25:'oris',26:'xori',
|
||||
27:'xoris',28:'andi.',29:'andis.',10:'cmpli',11:'cmpi',48:'lfs',50:'lfd',
|
||||
52:'stfs',54:'stfd'}
|
||||
FPOPS = {48, 49, 50, 51, 52, 53, 54, 55}
|
||||
|
||||
def dis(va, x):
|
||||
op = x >> 26; rD = (x >> 21) & 31; rA = (x >> 16) & 31; rB = (x >> 11) & 31
|
||||
imm = x & 0xFFFF; s = imm - 0x10000 if imm & 0x8000 else imm
|
||||
if op in DF:
|
||||
m = DF[op]; r = 'f' if op in FPOPS else 'r'
|
||||
if m in ('addi', 'addis') and rA == 0:
|
||||
return "li r%d,%d" % (rD, s)
|
||||
if m in ('ori', 'oris', 'xori', 'xoris', 'andi.', 'andis.'):
|
||||
return "%s r%d,r%d,0x%X" % (m, rA, rD, imm)
|
||||
if m in ('cmpli', 'cmpi'):
|
||||
return "%s cr%d,r%d,%s" % (m, (x >> 23) & 7, rA,
|
||||
hex(imm) if m == 'cmpli' else s)
|
||||
if m in ('addi', 'addis', 'addic', 'addic.', 'mulli', 'subfic'):
|
||||
return "%s r%d,r%d,%d" % (m, rD, rA, s)
|
||||
return "%s %s%d,%d(r%d)" % (m, r, rD, s, rA)
|
||||
if op in (20, 21):
|
||||
S, A, SH, MB, ME = rD, rA, (x >> 11) & 31, (x >> 6) & 31, (x >> 1) & 31
|
||||
return "%s r%d,r%d,%d,%d,%d ; mask=0x%08X" % (
|
||||
'rlwimi' if op == 20 else 'rlwinm', A, S, SH, MB, ME, _mask(MB, ME))
|
||||
if op == 31:
|
||||
e = (x >> 1) & 0x3FF; m = XO.get(e)
|
||||
if m is None:
|
||||
return ".long 0x%08X ; op31 xo=%d" % (x, e)
|
||||
if m in ('cmpw', 'cmplw'):
|
||||
return "%s cr%d,r%d,r%d" % (m, (x >> 23) & 7, rA, rB)
|
||||
if m == 'srawi':
|
||||
return "srawi r%d,r%d,%d" % (rA, rD, rB)
|
||||
if m in ('or', 'and', 'xor', 'andc', 'orc', 'nand', 'nor', 'slw', 'srw', 'sraw'):
|
||||
return "%s r%d,r%d,r%d" % (m, rA, rD, rB)
|
||||
if m in ('extsh', 'extsb', 'neg', 'cntlzw'):
|
||||
return "%s r%d,r%d" % (m, rA, rD)
|
||||
return "%s r%d,r%d,r%d" % (m, rD, rA, rB)
|
||||
if op == 18:
|
||||
li = x & 0x03FFFFFC
|
||||
if li & 0x02000000:
|
||||
li -= 0x04000000
|
||||
return "b%s 0x%08X" % ('l' if x & 1 else '', (va + li) & 0xFFFFFFFF)
|
||||
if op == 16:
|
||||
bo, bi = rD, rA
|
||||
bd = x & 0xFFFC
|
||||
if bd & 0x8000:
|
||||
bd -= 0x10000
|
||||
cond = {(12,0):'blt',(12,1):'bgt',(12,2):'beq',(4,0):'bge',(4,1):'ble',(4,2):'bne'}
|
||||
nm = cond.get((bo, bi & 3)) or cond.get((bo & 0x1E, bi & 3)) or "bc(%d,%d)" % (bo, bi)
|
||||
return "%s cr%d,0x%08X" % (nm, bi >> 2, (va + bd) & 0xFFFFFFFF)
|
||||
if op == 19:
|
||||
return {16: 'blr', 528: 'bctr'}.get((x >> 1) & 0x3FF, ".long 0x%08X" % x)
|
||||
if op in (59, 63):
|
||||
return "FP%d rD=%d rA=%d rB=%d rC=%d xo=%d" % (op, rD, rA, rB, (x >> 6) & 31,
|
||||
(x >> 1) & 0x1F)
|
||||
return ".long 0x%08X ; op=%d" % (x, op)
|
||||
|
||||
def main(argv):
|
||||
d = open(IMG, 'rb').read()
|
||||
if argv[0] == '--find-imm':
|
||||
want = int(argv[1], 0)
|
||||
lo = int(argv[2], 16) if len(argv) > 2 else BASE
|
||||
hi = int(argv[3], 16) if len(argv) > 3 else BASE + len(d)
|
||||
for a in range(lo, hi, 4):
|
||||
x = struct.unpack_from('>I', d, a - BASE)[0]
|
||||
if (x & 0xFFFF) == (want & 0xFFFF) and (x >> 26) in DF:
|
||||
print("%08X %08X %s" % (a, x, dis(a, x)))
|
||||
return 0
|
||||
a, e = int(argv[0], 16), int(argv[1], 16)
|
||||
while a < e:
|
||||
x = struct.unpack_from('>I', d, a - BASE)[0]
|
||||
print("%08X %08X %s" % (a, x, dis(a, x)))
|
||||
a += 4
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user