Compressed MonoGame XNB shaders and the Mac text box (general, from PR #120, found on Stardew Valley). shader_split.py: MonoGame's compressed XNB files, ContentCompressedLzx (flag 0x80: a CHMLib-style LZX decoder with a 2^16 window inside XNB block framing) and ContentCompressedLz4 (0x40: one raw LZ4 block), are decompressed before the readers (xnb_decompress); for LZX only the first block is decoded unless it names EffectReader. An MGFX shader whose length runs past the data is no effect (a red line in an XNB). Stardew Valley's 3,549 XNB files (3,535 LZ4, 8 LZX, 6 plain) all decode to their declared sizes, and its one content effect (Content/Effects/ShadowRemove.xnb, LZ4) is now split: 65 shaders, 64 before. Celeste's uncompressed XNBs give its profile's tables byte for byte. front_mac.m: while the text box is up the game window sees no key-up or mouse-up, so the button or click that opened it stayed pressed and was read the moment the box closed (Stardew Valley's character creation re-opened the keyboard after every OK); the keyboard buttons, the WASD stick and the touch are released when the box opens. The box also stays up, saying so, while the text is shorter than ShowKeyboardArg's minimum length (+0x3b0, hle_swkbd.c g_swkbd_min_chars), as the applet keeps OK disabled. Left out of the PR: writing the text's byte count into the String's +8. The SDK's ReadCloseResultAndString (Pikmin 4's sdk+0x39b7d0) reads String{pointer, capacity} and copies at most the capacity, and GetInteractiveOutStorageCallback restores +8 after changing it, so on hardware +8 stays the game's capacity; t_swkbd now checks it is unchanged on every path (17/17). Checked: Stardew Valley packs from its NSPs with a provisional profile and boots on the Mac to the title, and in character creation (DM_KBD) Name, Farm Name and Favorite Thing each take the text from one keyboard call and OK starts the intro, with +8 left as the capacity; the host unit tests and the 41 tool tests pass. Not checked: the Mac text box by hand (a scripted DM_KBD never shows it), and no game here asks for a minimum length. The NSP script's MonoGame survey note and docs/agents/04 say so
This commit is contained in:
1 parent
fc6c0a2a11
commit
fa69e20a28
6 files changed
+308
-18
No files matched your search
@@ -698,6 +698,267 @@ def bdt_shaders(path, rel, ents, out):
|
|||||||
for nm, b in bnd3_members(d, label):
|
for nm, b in bnd3_members(d, label):
|
||||||
if b[:4] == b'\x91\x68\x86\x19': out.extend(glslc_shaders(b, f'{label}/{nm}'))
|
if b[:4] == b'\x91\x68\x86\x19': out.extend(glslc_shaders(b, f'{label}/{nm}'))
|
||||||
|
|
||||||
|
class LzxError(Exception): pass
|
||||||
|
|
||||||
|
# MonoGame's compressed XNB (Stardew Valley: 3,535 of its 3,549 .xnb files): ContentManager.cs knows
|
||||||
|
# ContentCompressedLzx = 0x80 and ContentCompressedLz4 = 0x40 (XNA's LZX flag was 0x40, MonoGame's own writer
|
||||||
|
# uses these). The LZX is as XMemCompress writes it: a CHMLib-style LZX decoder (jedwing/CHMLib src/lzx.c, from
|
||||||
|
# cabextract) with a 2^16 window, inside XNB block framing (MonoGame's LzxDecoderStream): each block is two
|
||||||
|
# bytes, 0xff then the u16be output and input sizes, or the u16be input size with an 0x8000 output. The LZ4 is
|
||||||
|
# one raw LZ4 block (its Lz4DecoderStream)
|
||||||
|
LZX_EXTRA = (0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14,
|
||||||
|
15, 15, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17)
|
||||||
|
LZX_BASE = (0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072,
|
||||||
|
4096, 6144, 8192, 12288, 16384, 24576, 32768, 49152, 65536, 98304, 131072, 196608, 262144, 393216,
|
||||||
|
524288, 655360, 786432, 917504, 1048576, 1179648, 1310720, 1441792, 1572864, 1703936, 1835008, 1966080, 2097152)
|
||||||
|
|
||||||
|
def _lzx_table(nsyms, nbits, length):
|
||||||
|
"""make_decode_table: a huffman lookup table out of canonical code lengths (David Tritscher's, as in cabextract)"""
|
||||||
|
table = [0] * ((1 << nbits) + nsyms * 2)
|
||||||
|
pos, bit_num, table_mask = 0, 1, 1 << nbits
|
||||||
|
bit_mask = table_mask >> 1
|
||||||
|
next_symbol = bit_mask
|
||||||
|
while bit_num <= nbits:
|
||||||
|
for sym in range(nsyms):
|
||||||
|
if length[sym] == bit_num:
|
||||||
|
leaf = pos
|
||||||
|
pos += bit_mask
|
||||||
|
if pos > table_mask: raise LzxError('decode table overrun')
|
||||||
|
for i in range(leaf, pos): table[i] = sym
|
||||||
|
bit_mask >>= 1; bit_num += 1
|
||||||
|
if pos != table_mask:
|
||||||
|
for sym in range(pos, table_mask): table[sym] = 0
|
||||||
|
pos <<= 16; table_mask <<= 16; bit_mask = 1 << 15
|
||||||
|
while bit_num <= 16:
|
||||||
|
for sym in range(nsyms):
|
||||||
|
if length[sym] == bit_num:
|
||||||
|
leaf = pos >> 16
|
||||||
|
for fill in range(bit_num - nbits):
|
||||||
|
if table[leaf] == 0:
|
||||||
|
table[next_symbol << 1] = 0
|
||||||
|
table[(next_symbol << 1) + 1] = 0
|
||||||
|
table[leaf] = next_symbol; next_symbol += 1
|
||||||
|
leaf = table[leaf] << 1
|
||||||
|
if (pos >> (15 - fill)) & 1: leaf += 1
|
||||||
|
table[leaf] = sym
|
||||||
|
pos += bit_mask
|
||||||
|
if pos > table_mask: raise LzxError('decode table overflow')
|
||||||
|
bit_mask >>= 1; bit_num += 1
|
||||||
|
if pos != table_mask and any(length[:nsyms]): raise LzxError('a bad or incomplete huffman table')
|
||||||
|
return table
|
||||||
|
|
||||||
|
class LzxState:
|
||||||
|
"""The decoder state, which lives for the whole XNB: the ring window, the LRU offsets and the block in progress"""
|
||||||
|
def __init__(self, window_bits=16):
|
||||||
|
self.window_size = 1 << window_bits
|
||||||
|
self.window = bytearray(self.window_size)
|
||||||
|
self.posn = 0
|
||||||
|
self.R0 = self.R1 = self.R2 = 1
|
||||||
|
self.main_elements = 256 + ((window_bits << 1) << 3) # position slots: window<<1
|
||||||
|
self.header_read = False
|
||||||
|
self.block_type = 0
|
||||||
|
self.block_remaining = 0
|
||||||
|
self.intel_filesize = 0
|
||||||
|
self.intel_curpos = 0
|
||||||
|
self.intel_started = False
|
||||||
|
self.frames_read = 0
|
||||||
|
self.main_len = bytearray(256 + 50 * 8 + 64) # + LZX_LENTABLE_SAFETY: deltas are applied to them across blocks
|
||||||
|
self.length_len = bytearray(249 + 1 + 64)
|
||||||
|
self.main_tbl = self.length_tbl = self.al_tbl = None # an LZX block can span XNB frames, so the tables persist with the state
|
||||||
|
self.al_len = None
|
||||||
|
self.d = b''
|
||||||
|
self.ip = 0
|
||||||
|
self.bb = 0
|
||||||
|
self.bl = 0
|
||||||
|
|
||||||
|
def _ensure(self, n):
|
||||||
|
while self.bl < n:
|
||||||
|
self.bb = (self.bb << 16) | (self.d[self.ip] | (self.d[self.ip + 1] << 8) if self.ip + 1 < len(self.d) else 0) # a 16-bit overread past the block end is allowed, as in the C code
|
||||||
|
self.bl += 16; self.ip += 2
|
||||||
|
|
||||||
|
def _remove(self, n): self.bl -= n; self.bb &= (1 << self.bl) - 1
|
||||||
|
|
||||||
|
def _read(self, n):
|
||||||
|
self._ensure(n)
|
||||||
|
v = self.bb >> (self.bl - n)
|
||||||
|
self._remove(n)
|
||||||
|
return v
|
||||||
|
|
||||||
|
def _huffsym(self, table, nbits, maxsyms, lens):
|
||||||
|
self._ensure(16)
|
||||||
|
i = table[self.bb >> (self.bl - nbits)]
|
||||||
|
if i >= maxsyms:
|
||||||
|
k = 0
|
||||||
|
while True:
|
||||||
|
if self.bl - nbits - 1 - k < 0: raise LzxError('a huffman code longer than the bitstream')
|
||||||
|
i = (i << 1) | ((self.bb >> (self.bl - nbits - 1 - k)) & 1)
|
||||||
|
k += 1
|
||||||
|
i = table[i]
|
||||||
|
if i < maxsyms: break
|
||||||
|
self._remove(lens[i])
|
||||||
|
return i
|
||||||
|
|
||||||
|
def _read_lens(self, lens, first, last):
|
||||||
|
pre_len = [self._read(4) for _ in range(20)]
|
||||||
|
pre = _lzx_table(20, 6, pre_len)
|
||||||
|
x = first
|
||||||
|
while x < last:
|
||||||
|
z = self._huffsym(pre, 6, 20, pre_len)
|
||||||
|
if z == 17:
|
||||||
|
y = self._read(4) + 4
|
||||||
|
while y: lens[x] = 0; x += 1; y -= 1
|
||||||
|
elif z == 18:
|
||||||
|
y = self._read(5) + 20
|
||||||
|
while y: lens[x] = 0; x += 1; y -= 1
|
||||||
|
elif z == 19:
|
||||||
|
y = self._read(1) + 4
|
||||||
|
z = self._huffsym(pre, 6, 20, pre_len)
|
||||||
|
z = lens[x] - z
|
||||||
|
if z < 0: z += 17
|
||||||
|
while y: lens[x] = z; x += 1; y -= 1
|
||||||
|
else:
|
||||||
|
z = lens[x] - z
|
||||||
|
if z < 0: z += 17
|
||||||
|
lens[x] = z; x += 1
|
||||||
|
|
||||||
|
def decompress(self, src, outlen):
|
||||||
|
self.d, self.ip, self.bb, self.bl = src, 0, 0, 0
|
||||||
|
togo = outlen
|
||||||
|
if not self.header_read:
|
||||||
|
if self._read(1):
|
||||||
|
self.intel_filesize = (self._read(16) << 16) | self._read(16)
|
||||||
|
self.header_read = True
|
||||||
|
while togo > 0:
|
||||||
|
if self.block_remaining == 0:
|
||||||
|
if self.block_type == 3: # after an uncompressed block the bitstream returns to a byte boundary
|
||||||
|
if self.block_length & 1: self.ip += 1
|
||||||
|
self.bb = 0; self.bl = 0
|
||||||
|
self.block_type = self._read(3)
|
||||||
|
self.block_remaining = self.block_length = (self._read(16) << 8) | self._read(8)
|
||||||
|
al_table = pre = None
|
||||||
|
if self.block_type == 2:
|
||||||
|
self.al_len = [self._read(3) for _ in range(8)]
|
||||||
|
self.al_tbl = _lzx_table(8, 7, self.al_len)
|
||||||
|
if self.block_type in (1, 2):
|
||||||
|
self._read_lens(self.main_len, 0, 256) # the lengths are deltas against the previous block's, so they persist
|
||||||
|
self._read_lens(self.main_len, 256, self.main_elements)
|
||||||
|
self.main_tbl = _lzx_table(256 + 50 * 8, 12, self.main_len)
|
||||||
|
if self.main_len[0xE8]: self.intel_started = True
|
||||||
|
self._read_lens(self.length_len, 0, 249)
|
||||||
|
self.length_tbl = _lzx_table(249 + 1, 12, self.length_len)
|
||||||
|
elif self.block_type == 3:
|
||||||
|
self.intel_started = True
|
||||||
|
self._ensure(16)
|
||||||
|
if self.bl > 16: self.ip -= 2 # align the bitstream, as the C code does (the bits stay in the buffer)
|
||||||
|
self.R0 = int.from_bytes(self.d[self.ip:self.ip + 4], 'little'); self.ip += 4
|
||||||
|
self.R1 = int.from_bytes(self.d[self.ip:self.ip + 4], 'little'); self.ip += 4
|
||||||
|
self.R2 = int.from_bytes(self.d[self.ip:self.ip + 4], 'little'); self.ip += 4
|
||||||
|
else: raise LzxError(f'block type {self.block_type}')
|
||||||
|
if self.ip > len(self.d) and (self.ip > len(self.d) + 2 or self.bl < 16): raise LzxError('the block ended early')
|
||||||
|
this_run = min(self.block_remaining, togo)
|
||||||
|
togo -= this_run
|
||||||
|
self.block_remaining -= this_run
|
||||||
|
self.posn &= self.window_size - 1
|
||||||
|
if self.posn + this_run > self.window_size: raise LzxError('a run straddles the window')
|
||||||
|
posn = self.posn
|
||||||
|
if self.block_type == 3:
|
||||||
|
if self.ip + this_run > len(self.d): raise LzxError('the uncompressed block runs past the block')
|
||||||
|
self.window[posn:posn + this_run] = self.d[self.ip:self.ip + this_run]
|
||||||
|
self.ip += this_run; posn += this_run
|
||||||
|
else:
|
||||||
|
main, length, al_table, al_len = self.main_tbl, self.length_tbl, self.al_tbl, self.al_len
|
||||||
|
while this_run > 0:
|
||||||
|
main_element = self._huffsym(main, 12, 256 + 50 * 8, self.main_len)
|
||||||
|
if main_element < 256:
|
||||||
|
self.window[posn] = main_element; posn += 1; this_run -= 1
|
||||||
|
continue
|
||||||
|
main_element -= 256
|
||||||
|
match_length = main_element & 7
|
||||||
|
if match_length == 7:
|
||||||
|
match_length += self._huffsym(length, 12, 249 + 1, self.length_len)
|
||||||
|
match_length += 2
|
||||||
|
match_offset = main_element >> 3
|
||||||
|
if match_offset > 2:
|
||||||
|
if self.block_type == 2:
|
||||||
|
extra = LZX_EXTRA[match_offset]
|
||||||
|
match_offset = LZX_BASE[match_offset] - 2
|
||||||
|
if extra > 3:
|
||||||
|
extra -= 3
|
||||||
|
match_offset += (self._read(extra) << 3) + self._huffsym(al_table, 7, 8, al_len)
|
||||||
|
elif extra == 3:
|
||||||
|
match_offset += self._huffsym(al_table, 7, 8, al_len)
|
||||||
|
elif extra > 0:
|
||||||
|
match_offset += self._read(extra)
|
||||||
|
else:
|
||||||
|
match_offset = 1
|
||||||
|
else:
|
||||||
|
if match_offset != 3:
|
||||||
|
match_offset = LZX_BASE[match_offset] - 2 + self._read(LZX_EXTRA[match_offset])
|
||||||
|
else:
|
||||||
|
match_offset = 1
|
||||||
|
self.R2, self.R1, self.R0 = self.R1, self.R0, match_offset
|
||||||
|
elif match_offset == 0: match_offset = self.R0
|
||||||
|
elif match_offset == 1: match_offset = self.R1; self.R1, self.R0 = self.R0, match_offset
|
||||||
|
else: match_offset = self.R2; self.R2, self.R0 = self.R0, match_offset
|
||||||
|
posn += match_length
|
||||||
|
if posn > self.window_size: raise LzxError('a match overruns the window')
|
||||||
|
this_run -= match_length
|
||||||
|
src = posn - match_length - match_offset
|
||||||
|
if src < 0: src += self.window_size
|
||||||
|
if match_offset >= match_length and src + match_length <= self.window_size:
|
||||||
|
self.window[posn - match_length:posn] = self.window[src:src + match_length]
|
||||||
|
else:
|
||||||
|
w = self.window; dst = posn - match_length
|
||||||
|
for i in range(match_length): w[dst + i] = w[(src + i) % self.window_size]
|
||||||
|
self.posn = posn
|
||||||
|
start = self.posn - outlen if self.posn else self.window_size - outlen
|
||||||
|
out = bytes(self.window[start:self.posn or self.window_size])
|
||||||
|
self.frames_read += 1
|
||||||
|
if self.frames_read <= 32768 and self.intel_filesize != 0: # the Intel E8 transform
|
||||||
|
if outlen <= 6 or not self.intel_started:
|
||||||
|
self.intel_curpos += outlen
|
||||||
|
else:
|
||||||
|
data = bytearray(out)
|
||||||
|
curpos, filesize, pos, end = self.intel_curpos, self.intel_filesize, 0, outlen - 10
|
||||||
|
self.intel_curpos = curpos + outlen
|
||||||
|
while pos < end:
|
||||||
|
b = data[pos]; pos += 1
|
||||||
|
if b != 0xE8: curpos += 1; continue
|
||||||
|
abs_off = int.from_bytes(data[pos:pos + 4], 'little', signed=True)
|
||||||
|
if -curpos <= abs_off < filesize:
|
||||||
|
rel_off = abs_off - curpos if abs_off >= 0 else abs_off + filesize
|
||||||
|
data[pos:pos + 4] = (rel_off & 0xFFFFFFFF).to_bytes(4, 'little')
|
||||||
|
pos += 4
|
||||||
|
curpos += 5
|
||||||
|
out = bytes(data)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def xnb_decompress(d, first_block=False):
|
||||||
|
"""The decompressed content of a compressed XNB (flags 0x80 LZX, 0x40 LZ4; the u32 at 6 is the file size,
|
||||||
|
the u32 at 10 the decompressed size). first_block stops after the first LZX block: the reader names sit
|
||||||
|
at the start of the content, so an effect file is recognized there without decompressing the rest"""
|
||||||
|
dec_size = struct.unpack_from('<I', d, 10)[0]
|
||||||
|
body = d[14:struct.unpack_from('<I', d, 6)[0]]
|
||||||
|
if d[5] & 0x80:
|
||||||
|
st = LzxState(16)
|
||||||
|
out = bytearray(); p = 0
|
||||||
|
while len(out) < dec_size:
|
||||||
|
hi = body[p]; p += 1
|
||||||
|
if hi == 0xFF:
|
||||||
|
dst_size = (body[p] << 8) | body[p + 1]; src_size = (body[p + 2] << 8) | body[p + 3]; p += 4
|
||||||
|
else:
|
||||||
|
dst_size = 0x8000
|
||||||
|
src_size = (hi << 8) | body[p]; p += 1
|
||||||
|
if src_size == 0 or dst_size == 0: break
|
||||||
|
out += st.decompress(body[p:p + src_size], dst_size)
|
||||||
|
p += src_size
|
||||||
|
if first_block: break
|
||||||
|
return bytes(out)
|
||||||
|
if d[5] & 0x40:
|
||||||
|
import lz4.block
|
||||||
|
return lz4.block.decompress(body, uncompressed_size=dec_size)
|
||||||
|
return d[14:]
|
||||||
|
|
||||||
class _Rd: # a little-endian reader over bytes, with .NET's BinaryReader strings (7-bit encoded length + UTF-8)
|
class _Rd: # a little-endian reader over bytes, with .NET's BinaryReader strings (7-bit encoded length + UTF-8)
|
||||||
def __init__(s, d, p): s.d, s.p = d, p
|
def __init__(s, d, p): s.d, s.p = d, p
|
||||||
def u8(s): s.p += 1; return s.d[s.p - 1]
|
def u8(s): s.p += 1; return s.d[s.p - 1]
|
||||||
@@ -727,7 +988,11 @@ def mgfx_effect(d, p, label):
|
|||||||
if r.u8(): red(f'{label} MGFX @{p:#x}: annotations (MonoGame writes none)')
|
if r.u8(): red(f'{label} MGFX @{p:#x}: annotations (MonoGame writes none)')
|
||||||
out = []
|
out = []
|
||||||
for i in range(r.u8()):
|
for i in range(r.u8()):
|
||||||
vs, n = r.u8(), r.un('<I'); bc = r.raw(n)
|
vs, n = r.u8(), r.un('<I')
|
||||||
|
if r.p + n > len(d): # a shader whose length runs past the data: a stray 'MGFX' in other content, not an effect
|
||||||
|
if label.endswith('.xnb'): red(f'{label} MGFX @{p:#x}: shader {i} claims {n:#x} bytes past the end of the file')
|
||||||
|
return None
|
||||||
|
bc = r.raw(n)
|
||||||
nctl = struct.unpack_from('<I', bc, 0)[0]; ncode = struct.unpack_from('<I', bc, 4 + nctl)[0] if 8 + nctl <= n else -1
|
nctl = struct.unpack_from('<I', bc, 0)[0]; ncode = struct.unpack_from('<I', bc, 4 + nctl)[0] if 8 + nctl <= n else -1
|
||||||
if vs > 1 or 8 + nctl + ncode != n: red(f'{label} MGFX @{p:#x} shader {i}: {n:#x} bytes, not a control section and a code area')
|
if vs > 1 or 8 + nctl + ncode != n: red(f'{label} MGFX @{p:#x} shader {i}: {n:#x} bytes, not a control section and a code area')
|
||||||
ctl, code = bc[4:4 + nctl], bc[8 + nctl:]
|
ctl, code = bc[4:4 + nctl], bc[8 + nctl:]
|
||||||
@@ -1270,6 +1535,13 @@ def scan_file(args):
|
|||||||
return rel, uniq_keys(found), var_pairs(found), trees
|
return rel, uniq_keys(found), var_pairs(found), trees
|
||||||
d = unzs(path) if head[:4] == ZSTD_MAGIC else open(path, 'rb').read() # by the magic: Paper Mario: The Thousand-Year Door names its zstd files .zst (8,726, its shaders among them)
|
d = unzs(path) if head[:4] == ZSTD_MAGIC else open(path, 'rb').read() # by the magic: Paper Mario: The Thousand-Year Door names its zstd files .zst (8,726, its shaders among them)
|
||||||
if d[:4] == b'Yaz0': d = yaz0(d)
|
if d[:4] == b'Yaz0': d = yaz0(d)
|
||||||
|
if d[:3] == b'XNB' and d[4] == 5 and d[5] & 0xC0 and len(d) > 14: # a compressed XNB (Stardew Valley: 3,535 LZ4 files, 8 LZX): only effects (EffectReader) carry MGFX shaders, and for LZX the reader names sit in the first block, so it decides whether the rest is worth decoding
|
||||||
|
if d[5] & 0x80:
|
||||||
|
head_d = xnb_decompress(d, first_block=True)
|
||||||
|
if b'EffectReader' not in head_d: return rel, {}, set(), {}
|
||||||
|
d = head_d if len(head_d) >= struct.unpack_from('<I', d, 10)[0] else xnb_decompress(d)
|
||||||
|
else:
|
||||||
|
d = xnb_decompress(d)
|
||||||
if d[:8] == b'UnityFS\0' or (d[8:11] == b'\0\0\0' and __import__('unityfs').is_serialized(d)):
|
if d[:8] == b'UnityFS\0' or (d[8:11] == b'\0\0\0' and __import__('unityfs').is_serialized(d)):
|
||||||
import unityfs
|
import unityfs
|
||||||
try: unity_shaders(d, '/' + rel, found, trees if not TREES else dict(TREES))
|
try: unity_shaders(d, '/' + rel, found, trees if not TREES else dict(TREES))
|
||||||
|
|||||||
+21
-10
@@ -273,22 +273,33 @@ void front_run(void (*guest)(void)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Software keyboard input box (front.h): the main thread shows an NSAlert + text field and the guest thread waits synchronously for the result. Not shown in windowless mode (returns -1, the caller treats it as cancel)
|
// Software keyboard input box (front.h): the main thread shows an NSAlert + text field and the guest thread waits synchronously for the result. Not shown in windowless mode (returns -1, the caller treats it as cancel)
|
||||||
|
extern int g_swkbd_min_chars; // hle_swkbd.c: this input's minimum length from ShowKeyboardArg; the applet keeps OK disabled until the text is long enough
|
||||||
int front_text_input(const char *title, const char *sub, const char *guide, const char *initial, int max_chars, char *out, int out_cap) {
|
int front_text_input(const char *title, const char *sub, const char *guide, const char *initial, int max_chars, char *out, int out_cap) {
|
||||||
if (!g_layer) return -1;
|
if (!g_layer) return -1;
|
||||||
__block int r = 0;
|
__block int r = 0;
|
||||||
__block NSString *text = nil;
|
__block NSString *text = nil;
|
||||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||||
NSAlert *a = [NSAlert new];
|
// The box takes the keyboard and the mouse, so the game window never sees the key-up of the button or the touch-up of the click that opened it: nothing may stay pressed
|
||||||
a.messageText = title && *title ? @(title) : @"Input";
|
// while it is up, or the game reads the stuck button / touch the moment the box closes (Stardew Valley's character creation re-opened the keyboard endlessly)
|
||||||
|
g_btn = 0; g_w = g_a = g_s = g_d = 0; push();
|
||||||
|
dm_touch_set(NULL, 0);
|
||||||
|
int min_chars = g_swkbd_min_chars > 0 ? g_swkbd_min_chars : 0;
|
||||||
NSString *info = [NSString stringWithFormat:@"%s%s%s", sub ? sub : "", sub && *sub && guide && *guide ? "\n" : "", guide ? guide : ""];
|
NSString *info = [NSString stringWithFormat:@"%s%s%s", sub ? sub : "", sub && *sub && guide && *guide ? "\n" : "", guide ? guide : ""];
|
||||||
a.informativeText = info;
|
for (;;) {
|
||||||
[a addButtonWithTitle:@"OK"]; [a addButtonWithTitle:@"Cancel"];
|
NSAlert *a = [NSAlert new];
|
||||||
NSTextField *f = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 24)];
|
a.messageText = title && *title ? @(title) : @"Input";
|
||||||
f.stringValue = initial ? @(initial) : @"";
|
a.informativeText = info;
|
||||||
if (guide && *guide) f.placeholderString = @(guide);
|
[a addButtonWithTitle:@"OK"]; [a addButtonWithTitle:@"Cancel"];
|
||||||
a.accessoryView = f; a.window.initialFirstResponder = f;
|
NSTextField *f = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 24)];
|
||||||
r = [a runModal] == NSAlertFirstButtonReturn;
|
f.stringValue = text ?: (initial ? @(initial) : @"");
|
||||||
text = f.stringValue;
|
if (guide && *guide) f.placeholderString = @(guide);
|
||||||
|
a.accessoryView = f; a.window.initialFirstResponder = f;
|
||||||
|
r = [a runModal] == NSAlertFirstButtonReturn;
|
||||||
|
text = f.stringValue;
|
||||||
|
if (!r || (int)text.length >= min_chars) break;
|
||||||
|
// On hardware the applet would not have accepted the shorter text (OK stayed disabled); a game that rejects it re-opens the keyboard for ever, so the box comes back with the rule instead
|
||||||
|
info = [info stringByAppendingFormat:@"\nAt least %d characters are needed here.", min_chars];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (!r) return 0;
|
if (!r) return 0;
|
||||||
(void)max_chars; // Truncation is done uniformly in hle_swkbd.c in UTF-16 units
|
(void)max_chars; // Truncation is done uniformly in hle_swkbd.c in UTF-16 units
|
||||||
|
|||||||
+7
-5
@@ -5,7 +5,7 @@
|
|||||||
// +0x3ac maximum length, +0x3b0 minimum length, +0x3bd isUseUtf8, +0x3c0 offset of the initial text within the work buffer, +0x3c4 UTF-16 length of the initial text;
|
// +0x3ac maximum length, +0x3b0 minimum length, +0x3bd isUseUtf8, +0x3c0 offset of the initial text within the work buffer, +0x3c4 UTF-16 length of the initial text;
|
||||||
// work buffer pointer / size at +0x4d0 / +0x4d8 (handed to the applet together with CreateLargeStorage)
|
// work buffer pointer / size at +0x4d0 / +0x4d8 (handed to the applet together with CreateLargeStorage)
|
||||||
// SetInitialTextUtf8 0x3a74c0: UTF-8 → UTF-16 written to work buffer+offset (offset 0x14, at most 500 characters + terminating 0)
|
// SetInitialTextUtf8 0x3a74c0: UTF-8 → UTF-16 written to work buffer+offset (offset 0x14, at most 500 characters + terminating 0)
|
||||||
// ReadCloseResultAndString 0x3a63f0: output storage = u32 CloseResult + text area; the text area is copied as is into String{+0 pointer, +8 byte count}
|
// ReadCloseResultAndString 0x3a63f0: output storage = u32 CloseResult + text area; the text area is copied as is into String{+0 pointer, +8 capacity}, at most the capacity, which it only reads (Pikmin 4: sdk+0x39b7d0)
|
||||||
// ShowKeyboard wrap-up 0x3a6acc: CloseResult == 0 (OK) → Result 0, otherwise (cancel) → 0x29F; GetRequiredStringBufferSize = 0x7D4
|
// ShowKeyboard wrap-up 0x3a6acc: CloseResult == 0 (OK) → Result 0, otherwise (cancel) → 0x29F; GetRequiredStringBufferSize = 0x7D4
|
||||||
// A game that sets isUseUtf8 = 1 after MakePreset reads the result back as a C string; UTF-16 output is supported as well per the original convention.
|
// A game that sets isUseUtf8 = 1 after MakePreset reads the result back as a C string; UTF-16 output is supported as well per the original convention.
|
||||||
// No window and DM_KBD unset: return as cancel (no popup, no hang). DM_KBD="text": no popup, return that text directly as OK (windowless testing / lockstep comparison).
|
// No window and DM_KBD unset: return as cancel (no popup, no hang). DM_KBD="text": no popup, return that text directly as OK (windowless testing / lockstep comparison).
|
||||||
@@ -56,6 +56,7 @@ size_t swkbd_u8_to_u16(const char *s, size_t max_units, uint16_t *out, size_t ca
|
|||||||
// hle_charname.c also lets bytes ≥0x80 through (Chinese names), so the rule becomes: English letters or Chinese characters, at most 15 UTF-8 bytes in total (3 bytes per Chinese character, at most 5).
|
// hle_charname.c also lets bytes ≥0x80 through (Chinese names), so the rule becomes: English letters or Chinese characters, at most 15 UTF-8 bytes in total (3 bytes per Chinese character, at most 5).
|
||||||
// While the naming panel is open the front end blocks on the spot by this rule and writes the rule in the dialog. diag.c maintains this flag from UI messages.
|
// While the naming panel is open the front end blocks on the spot by this rule and writes the rule in the dialog. diag.c maintains this flag from UI messages.
|
||||||
int g_swkbd_name_panel;
|
int g_swkbd_name_panel;
|
||||||
|
int g_swkbd_min_chars = -1; // ≥0 = this input needs at least that many characters (ShowKeyboardArg's minimum length; the applet keeps OK disabled until the text is long enough, so the front end's box keeps itself up until then)
|
||||||
int g_swkbd_numeric_max = -1; // ≥0 = this input is a quantity box, value = upper limit (set by dm_textinput.c, read by the front end input box, set back to -1 after use)
|
int g_swkbd_numeric_max = -1; // ≥0 = this input is a quantity box, value = upper limit (set by dm_textinput.c, read by the front end input box, set back to -1 after use)
|
||||||
int (*g_swkbd_unfilter)(void); // registered by the host (dm_charname_unfilter in hle_charname.c; NULL in unit tests): keyboard requests from the naming panel bypass the game's ASCII typing filter
|
int (*g_swkbd_unfilter)(void); // registered by the host (dm_charname_unfilter in hle_charname.c; NULL in unit tests): keyboard requests from the naming panel bypass the game's ASCII typing filter
|
||||||
typedef int (*swkbd_input_fn)(const char *title, const char *sub, const char *guide, const char *initial, int max_chars, char *out, int out_cap);
|
typedef int (*swkbd_input_fn)(const char *title, const char *sub, const char *guide, const char *initial, int max_chars, char *out, int out_cap);
|
||||||
@@ -65,7 +66,7 @@ Result swkbd_show(void *out, const void *arg_, swkbd_input_fn input) {
|
|||||||
swkbd_u16_to_u8((const uint16_t *)(arg + ARG_HEADER), HEADER_N, title, sizeof title);
|
swkbd_u16_to_u8((const uint16_t *)(arg + ARG_HEADER), HEADER_N, title, sizeof title);
|
||||||
swkbd_u16_to_u8((const uint16_t *)(arg + ARG_SUB), SUB_N, sub, sizeof sub);
|
swkbd_u16_to_u8((const uint16_t *)(arg + ARG_SUB), SUB_N, sub, sizeof sub);
|
||||||
swkbd_u16_to_u8((const uint16_t *)(arg + ARG_GUIDE), GUIDE_N, guide, sizeof guide);
|
swkbd_u16_to_u8((const uint16_t *)(arg + ARG_GUIDE), GUIDE_N, guide, sizeof guide);
|
||||||
int32_t maxlen, ilen, ioff; memcpy(&maxlen, arg + ARG_MAXLEN, 4); memcpy(&ilen, arg + ARG_INIT_LEN, 4); memcpy(&ioff, arg + ARG_INIT_OFF, 4);
|
int32_t maxlen, minlen, ilen, ioff; memcpy(&maxlen, arg + ARG_MAXLEN, 4); memcpy(&minlen, arg + ARG_MINLEN, 4); memcpy(&ilen, arg + ARG_INIT_LEN, 4); memcpy(&ioff, arg + ARG_INIT_OFF, 4);
|
||||||
const uint8_t *work; memcpy(&work, arg + ARG_WORKBUF, 8);
|
const uint8_t *work; memcpy(&work, arg + ARG_WORKBUF, 8);
|
||||||
if (ilen > 0 && work) swkbd_u16_to_u8((const uint16_t *)(work + ioff), (size_t)ilen, init, sizeof init);
|
if (ilen > 0 && work) swkbd_u16_to_u8((const uint16_t *)(work + ioff), (size_t)ilen, init, sizeof init);
|
||||||
|
|
||||||
@@ -77,9 +78,10 @@ Result swkbd_show(void *out, const void *arg_, swkbd_input_fn input) {
|
|||||||
// Test switch DM_TEST_SWKBD_HOLD_MS=<ms> (with DM_KBD): the keyboard stays up that long before DM_KBD's text is returned, the way a player types into the input box (for reproducing on the Mac what the game's other threads do meanwhile)
|
// Test switch DM_TEST_SWKBD_HOLD_MS=<ms> (with DM_KBD): the keyboard stays up that long before DM_KBD's text is returned, the way a player types into the input box (for reproducing on the Mac what the game's other threads do meanwhile)
|
||||||
static int hold = -1; if (hold < 0) { const char *h = getenv("DM_TEST_SWKBD_HOLD_MS"); hold = h ? atoi(h) : 0; }
|
static int hold = -1; if (hold < 0) { const char *h = getenv("DM_TEST_SWKBD_HOLD_MS"); hold = h ? atoi(h) : 0; }
|
||||||
if (script && hold > 0) usleep((useconds_t)hold * 1000);
|
if (script && hold > 0) usleep((useconds_t)hold * 1000);
|
||||||
int r = script ? (snprintf(text, sizeof text, "%s", script), 1) : input(title, sub, guide, init, maxlen > 0 ? maxlen : 0, text, sizeof text);
|
int r = script ? (snprintf(text, sizeof text, "%s", script), 1) : (g_swkbd_min_chars = minlen > 0 ? minlen : -1, input(title, sub, guide, init, maxlen > 0 ? maxlen : 0, text, sizeof text));
|
||||||
g_swkbd_numeric_max = -1; // the numeric box mark applies to this one time only (set by dm_textinput.c before bringing up the keyboard)
|
g_swkbd_numeric_max = -1; // the numeric box mark applies to this one time only (set by dm_textinput.c before bringing up the keyboard)
|
||||||
LOG("swkbd: title '%s' initial '%s' limit %d → %s%s%s", title, init, maxlen,
|
g_swkbd_min_chars = -1; // the minimum length applies to this one time only
|
||||||
|
LOG("swkbd: title '%s' initial '%s' limit %d min %d → %s%s%s", title, init, maxlen, minlen,
|
||||||
r > 0 ? "OK '" : r == 0 ? "cancel" : "no window, treated as cancel", r > 0 ? text : "", r > 0 ? "'" : "");
|
r > 0 ? "OK '" : r == 0 ? "cancel" : "no window, treated as cancel", r > 0 ? text : "", r > 0 ? "'" : "");
|
||||||
if (r <= 0) return SWKBD_RESULT_CANCELED;
|
if (r <= 0) return SWKBD_RESULT_CANCELED;
|
||||||
if (g_swkbd_name_panel && g_swkbd_unfilter && g_swkbd_unfilter()) LOG("swkbd: naming panel -- turned off the game's ASCII input filter on the returned string (Chinese names allowed, see hle_charname.c)");
|
if (g_swkbd_name_panel && g_swkbd_unfilter && g_swkbd_unfilter()) LOG("swkbd: naming panel -- turned off the game's ASCII input filter on the returned string (Chinese names allowed, see hle_charname.c)");
|
||||||
@@ -88,7 +90,7 @@ Result swkbd_show(void *out, const void *arg_, swkbd_input_fn input) {
|
|||||||
if (!buf || !cap) return RES_OK; // same as the SDK: if String has no buffer, the text is not read
|
if (!buf || !cap) return RES_OK; // same as the SDK: if String has no buffer, the text is not read
|
||||||
memset(buf, 0, cap);
|
memset(buf, 0, cap);
|
||||||
uint16_t u16[1003]; size_t n = swkbd_u8_to_u16(text, maxlen > 0 ? (size_t)maxlen : 1002, u16, 1003); // truncate to the character limit (counted in UTF-16 units)
|
uint16_t u16[1003]; size_t n = swkbd_u8_to_u16(text, maxlen > 0 ? (size_t)maxlen : 1002, u16, 1003); // truncate to the character limit (counted in UTF-16 units)
|
||||||
if (arg[ARG_UTF8]) swkbd_u16_to_u8(u16, n, (char *)buf, cap);
|
if (arg[ARG_UTF8]) swkbd_u16_to_u8(u16, n, (char *)buf, cap); // String's +8 stays the game's capacity: ReadCloseResultAndString only reads it (Pikmin 4's sdk+0x39b81c)
|
||||||
else memcpy(buf, u16, (n + 1) * 2 <= cap ? n * 2 : (cap / 2 - 1) * 2);
|
else memcpy(buf, u16, (n + 1) * 2 <= cap ? n * 2 : (cap / 2 - 1) * 2);
|
||||||
{ const char *lag = getenv("DM_TEST_SWKBD_LAG_MS"); if (lag) usleep((useconds_t)atoi(lag) * 1000); } // test: slow down "keyboard handed back → the game's thread gets the result" to verify that auto-accept does not jump the gun // front end path cleanup (dm_cursor.c): auto-accept after the name is typed on the naming page
|
{ const char *lag = getenv("DM_TEST_SWKBD_LAG_MS"); if (lag) usleep((useconds_t)atoi(lag) * 1000); } // test: slow down "keyboard handed back → the game's thread gets the result" to verify that auto-accept does not jump the gun // front end path cleanup (dm_cursor.c): auto-accept after the name is typed on the naming page
|
||||||
return RES_OK;
|
return RES_OK;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ static uint8_t arg[0x500], work[0x1000], sbuf[0x7d4];
|
|||||||
static struct { uint8_t *p; uint64_t n; } str = {sbuf, sizeof sbuf};
|
static struct { uint8_t *p; uint64_t n; } str = {sbuf, sizeof sbuf};
|
||||||
static void mkarg(int utf8, int maxlen, const uint16_t *init, int ilen) {
|
static void mkarg(int utf8, int maxlen, const uint16_t *init, int ilen) {
|
||||||
memset(arg, 0, sizeof arg); memset(work, 0, sizeof work); memset(sbuf, 0xAA, sizeof sbuf);
|
memset(arg, 0, sizeof arg); memset(work, 0, sizeof work); memset(sbuf, 0xAA, sizeof sbuf);
|
||||||
|
str.n = sizeof sbuf; // a fresh String per call, its capacity at +8 (the SDK only reads it)
|
||||||
static const uint16_t title[] = {0x8D77, 0x540D, 0}; // U+8D77 U+540D, the game's "Name your character" title
|
static const uint16_t title[] = {0x8D77, 0x540D, 0}; // U+8D77 U+540D, the game's "Name your character" title
|
||||||
memcpy(arg + 0x24, title, sizeof title);
|
memcpy(arg + 0x24, title, sizeof title);
|
||||||
memcpy(arg + 0x3ac, &maxlen, 4); arg[0x3bd] = (uint8_t)utf8;
|
memcpy(arg + 0x3ac, &maxlen, 4); arg[0x3bd] = (uint8_t)utf8;
|
||||||
@@ -46,6 +47,7 @@ int main(void) {
|
|||||||
static const uint8_t w1[] = {0xC3, 0x9C, 0x6E, 0xC3, 0xAF, 0xE5, 0x90, 0x8D, 0x00}; // U+00DC n U+00EF U+540D + terminating 0
|
static const uint8_t w1[] = {0xC3, 0x9C, 0x6E, 0xC3, 0xAF, 0xE5, 0x90, 0x8D, 0x00}; // U+00DC n U+00EF U+540D + terminating 0
|
||||||
CHECK(r == 0, "Result %#x expected 0", r);
|
CHECK(r == 0, "Result %#x expected 0", r);
|
||||||
CHECK(bytes_eq(w1, sizeof w1), "UTF-8 write-back bytes differ");
|
CHECK(bytes_eq(w1, sizeof w1), "UTF-8 write-back bytes differ");
|
||||||
|
CHECK(str.n == sizeof sbuf, "String byte count %llu changed, the SDK leaves the capacity", (unsigned long long)str.n);
|
||||||
CHECK(!strcmp(g_title, "\xE8\xB5\xB7\xE5\x90\x8D"), "title decoded as '%s', expected U+8D77 U+540D", g_title);
|
CHECK(!strcmp(g_title, "\xE8\xB5\xB7\xE5\x90\x8D"), "title decoded as '%s', expected U+8D77 U+540D", g_title);
|
||||||
CHECK(!strcmp(g_init, "Hero"), "initial text decoded as '%s', expected Hero", g_init);
|
CHECK(!strcmp(g_init, "Hero"), "initial text decoded as '%s', expected Hero", g_init);
|
||||||
CHECK(g_max == 16, "limit %d expected 16", g_max);
|
CHECK(g_max == 16, "limit %d expected 16", g_max);
|
||||||
@@ -55,6 +57,7 @@ int main(void) {
|
|||||||
r = swkbd_show(&str, arg, stub);
|
r = swkbd_show(&str, arg, stub);
|
||||||
CHECK(r == 0x29F, "Result %#x expected 0x29F", r);
|
CHECK(r == 0x29F, "Result %#x expected 0x29F", r);
|
||||||
CHECK(sbuf[0] == 0xAA && sbuf[sizeof sbuf - 1] == 0xAA, "cancel changed the buffer");
|
CHECK(sbuf[0] == 0xAA && sbuf[sizeof sbuf - 1] == 0xAA, "cancel changed the buffer");
|
||||||
|
CHECK(str.n == sizeof sbuf, "cancel changed the String byte count");
|
||||||
|
|
||||||
printf("3 no window (input returns -1) → treated as cancel 0x29F\n");
|
printf("3 no window (input returns -1) → treated as cancel 0x29F\n");
|
||||||
mkarg(1, 16, hero, 4); g_ret = -1;
|
mkarg(1, 16, hero, 4); g_ret = -1;
|
||||||
@@ -65,6 +68,7 @@ int main(void) {
|
|||||||
r = swkbd_show(&str, arg, stub);
|
r = swkbd_show(&str, arg, stub);
|
||||||
static const uint8_t w4[] = {0x41, 0x00, 0x0D, 0x54, 0x00, 0x00}; // 'A' U+540D terminating 0 (little endian)
|
static const uint8_t w4[] = {0x41, 0x00, 0x0D, 0x54, 0x00, 0x00}; // 'A' U+540D terminating 0 (little endian)
|
||||||
CHECK(r == 0 && bytes_eq(w4, sizeof w4), "UTF-16 write-back bytes differ");
|
CHECK(r == 0 && bytes_eq(w4, sizeof w4), "UTF-16 write-back bytes differ");
|
||||||
|
CHECK(str.n == sizeof sbuf, "String byte count %llu changed, the SDK leaves the capacity", (unsigned long long)str.n);
|
||||||
CHECK(g_init[0] == 0, "initial length 0 should give no initial text");
|
CHECK(g_init[0] == 0, "initial length 0 should give no initial text");
|
||||||
|
|
||||||
printf("5 maximum length truncation (3 characters)\n");
|
printf("5 maximum length truncation (3 characters)\n");
|
||||||
@@ -72,6 +76,7 @@ int main(void) {
|
|||||||
swkbd_show(&str, arg, stub);
|
swkbd_show(&str, arg, stub);
|
||||||
static const uint8_t w5[] = {'A', 'B', 'C', 0};
|
static const uint8_t w5[] = {'A', 'B', 'C', 0};
|
||||||
CHECK(bytes_eq(w5, sizeof w5), "truncation result differs");
|
CHECK(bytes_eq(w5, sizeof w5), "truncation result differs");
|
||||||
|
CHECK(str.n == sizeof sbuf, "String byte count %llu changed, the SDK leaves the capacity", (unsigned long long)str.n);
|
||||||
|
|
||||||
printf("6 truncation does not split a surrogate pair (limit 2 units, a + U+1F600 needs 3 units)\n");
|
printf("6 truncation does not split a surrogate pair (limit 2 units, a + U+1F600 needs 3 units)\n");
|
||||||
mkarg(1, 2, hero, 4); g_ret = 1; g_reply = "a\xF0\x9F\x98\x80";
|
mkarg(1, 2, hero, 4); g_ret = 1; g_reply = "a\xF0\x9F\x98\x80";
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Do not stop at the obvious archive. In Mario the main `bfsha` held 4,888 of the
|
|||||||
- **BNSH in Bezel Engine archives** (Big Brain Academy: Brain vs. Brain, NintendoWare Bezel Engine 1.56): the shaders are BNSH in the `.bfsha` entries of the `.bea` archives (zstd entries, [01](01-extract.md)), most of them the engine's own in `System.nx.bea` (`_BezelSystemResources/<system>/shader/*.bfsha`: PBR shadow maps 846, post effects 770, …), the rest in the layouts' `__ArchiveShader.bnsh` and the per-archive shader packs (`.bnbshpk`), plus one BNSH in `main`. `shader_split.py` scans each archive entry as a file of its own (`bea_members`): 2,125 shaders in about 90 s.
|
- **BNSH in Bezel Engine archives** (Big Brain Academy: Brain vs. Brain, NintendoWare Bezel Engine 1.56): the shaders are BNSH in the `.bfsha` entries of the `.bea` archives (zstd entries, [01](01-extract.md)), most of them the engine's own in `System.nx.bea` (`_BezelSystemResources/<system>/shader/*.bfsha`: PBR shadow maps 846, post effects 770, …), the rest in the layouts' `__ArchiveShader.bnsh` and the per-archive shader packs (`.bnbshpk`), plus one BNSH in `main`. `shader_split.py` scans each archive entry as a file of its own (`bea_members`): 2,125 shaders in about 90 s.
|
||||||
- **SPK in an MT Framework ARC** (The Great Ace Attorney Chronicles, Capcom's MT Framework): the engine's one shader package, `sc\NX_64\root` (`SPK\0`, type hash `0x02358e1a`) in `nativeNXx64/sa/NX_64/root.arc` ([01](01-extract.md)). After the package's tables every shader is a code area followed directly by its control section, back to back to the end of the file: 230 vertex shaders, then 230 pixel shaders. Each control section names its code length (+0x704), so the code is the bytes right before it, and every block must start where the previous one ended (`mtspk_shaders`). The tables that say which vertex and pixel shaders the engine combines are not read: the vertex shaders are paired with the single-stage cover (one pixel shader reads every input any of them reads). `CustomShaderPackage.mfx` (`MFX\0`, the effect descriptions) holds no shader code, and neither `main` nor any other ARC entry does. All 460 build, with Mario's binding layout; every binding decodes.
|
- **SPK in an MT Framework ARC** (The Great Ace Attorney Chronicles, Capcom's MT Framework): the engine's one shader package, `sc\NX_64\root` (`SPK\0`, type hash `0x02358e1a`) in `nativeNXx64/sa/NX_64/root.arc` ([01](01-extract.md)). After the package's tables every shader is a code area followed directly by its control section, back to back to the end of the file: 230 vertex shaders, then 230 pixel shaders. Each control section names its code length (+0x704), so the code is the bytes right before it, and every block must start where the previous one ended (`mtspk_shaders`). The tables that say which vertex and pixel shaders the engine combines are not read: the vertex shaders are paired with the single-stage cover (one pixel shader reads every input any of them reads). `CustomShaderPackage.mfx` (`MFX\0`, the effect descriptions) holds no shader code, and neither `main` nor any other ARC entry does. All 460 build, with Mario's binding layout; every binding decodes.
|
||||||
- **Control sections loose in `main`** (Advance Wars 1+2: Re-Boot Camp, Unity 2020.2 with Bink 2's Unity plugin): the movie player builds its programs from 32 control sections and code areas in `main`'s read-only segment that are in no GLSLC output and no BNSH file. 26 are 0x20-byte records in two tables in `.data` (`{code area pointer, control section pointer, u32 code length, u32 control length}`, both pointers relative relocations); the code takes the other 6 by address (`adrp` + `add` to the control section and, a few instructions away, to the code area it copies with `memcpy`), the code area right after the control section's own length (+0x20, 16-byte aligned) or right before it, and two control sections share one code area. Both neighbours can pass the header and length checks, so the reader (`raw_shaders`) takes only a code area the code builds the address of, the nearest one to the control section's. The 8 compute shaders' code areas have no header (SDK 10). Without them the shaders were not in the map (two `[RED] shader … is not in the shader map` lines and `program has no vertex shader` as the opening movie starts) and every movie drew white. Of 25 `main`s on hand no other has such control sections.
|
- **Control sections loose in `main`** (Advance Wars 1+2: Re-Boot Camp, Unity 2020.2 with Bink 2's Unity plugin): the movie player builds its programs from 32 control sections and code areas in `main`'s read-only segment that are in no GLSLC output and no BNSH file. 26 are 0x20-byte records in two tables in `.data` (`{code area pointer, control section pointer, u32 code length, u32 control length}`, both pointers relative relocations); the code takes the other 6 by address (`adrp` + `add` to the control section and, a few instructions away, to the code area it copies with `memcpy`), the code area right after the control section's own length (+0x20, 16-byte aligned) or right before it, and two control sections share one code area. Both neighbours can pass the header and length checks, so the reader (`raw_shaders`) takes only a code area the code builds the address of, the nearest one to the control section's. The 8 compute shaders' code areas have no header (SDK 10). Without them the shaders were not in the map (two `[RED] shader … is not in the shader map` lines and `program has no vertex shader` as the opening movie starts) and every movie drew white. Of 25 `main`s on hand no other has such control sections.
|
||||||
- **MonoGame effects** (Celeste: MonoGame for the Switch, its C# compiled to C++ by Sickhead's BRUTE): the 13 XNB files in `Content/Effects` (XNB platform `S`, uncompressed, read by `EffectReader`) each hold one MGFX effect, and `main` holds SpriteBatch's built-in one; no other file and no GLSLC output. MGFX version 8 is MonoGame's own layout: `MGFX`, u8 version, u8 profile, u32 effect key, the constant buffers, then each shader as u8 vertex (1) / pixel (0), u32 length, the bytecode (u32 control length, the control section, u32 code length, the code area: GLSLC's output without its header), the samplers, the constant buffer indices and an attribute list (as MonoGame's OpenGL backend writes it), then the parameters and the techniques with their passes (`mgfx_effect`). The reader parses every effect to its end, which checks the layout; in `main`, four bytes `MGFX` count only when version 8 and a control section follow. A pass names its vertex and pixel shader by index, 255 for none, and 10 of the 13 effects have no vertex shader: SpriteBatch binds its own and the effect's pass only replaces the pixel shader. Vertex and pixel shaders therefore combine across effects, and they are paired as single-stage programs. 26 shaders (20 pixel, 6 vertex), all with Mario's binding layout; every binding decodes.
|
- **MonoGame effects** (Celeste: MonoGame for the Switch, its C# compiled to C++ by Sickhead's BRUTE): the 13 XNB files in `Content/Effects` (XNB platform `S`, uncompressed, read by `EffectReader`; a compressed XNB, MonoGame's LZX flag 0x80 or LZ4 flag 0x40 as in Stardew Valley, is decompressed first, `xnb_decompress`) each hold one MGFX effect, and `main` holds SpriteBatch's built-in one; no other file and no GLSLC output. MGFX version 8 is MonoGame's own layout: `MGFX`, u8 version, u8 profile, u32 effect key, the constant buffers, then each shader as u8 vertex (1) / pixel (0), u32 length, the bytecode (u32 control length, the control section, u32 code length, the code area: GLSLC's output without its header), the samplers, the constant buffer indices and an attribute list (as MonoGame's OpenGL backend writes it), then the parameters and the techniques with their passes (`mgfx_effect`). The reader parses every effect to its end, which checks the layout; in `main`, four bytes `MGFX` count only when version 8 and a control section follow. A pass names its vertex and pixel shader by index, 255 for none, and 10 of the 13 effects have no vertex shader: SpriteBatch binds its own and the effect's pass only replaces the pixel shader. Vertex and pixel shaders therefore combine across effects, and they are paired as single-stage programs. 26 shaders (20 pixel, 6 vertex), all with Mario's binding layout; every binding decodes.
|
||||||
- **GLSLC outputs in FromSoftware's dvdbnd archives** (Dark Souls Remastered): RomFS is two archives, `dvdbnd0.bdt` and `dvdbnd1.bdt` (3.6 GB), each indexed by the `.bhd5` beside it: `BHD5`, 0xff (little-endian), u32 1, u32 file size, u32 bucket count, u32 0, u64 bucket table offset; each 16-byte bucket `{u32 count, u32, u64 offset}` points at 16-byte entries `{u32 path hash, u32 size, u64 offset}` (paths only hashed). 5,184 entries are DCX containers (`DCX\0`, big-endian header, `DFLT` = a zlib stream at +0x4c), most of them BND3 archives (28-byte entries `{u8 flags, i32 size, u64 offset, i32 ID, u32 name offset, i32 unpacked size}` in formats 0x5c / 0x7c). Six BND3 archives in `dvdbnd0.bdt` hold the shaders, one GLSLC output per entry (`.vpo` vertex, `.fpo` pixel, named `Shader/FRPG_Sfx/NVNShaderBinary/ps/…` or by base name in the two FLVER material archives, 2,602 + 486 entries): `bhd5_entries`, `undcx`, `bnd3_members`, `bdt_shaders` in `shader_split.py`, in 64 jobs by entry range, about 10 s. Only archives that contain a GLSLC magic are parsed (the others use format 0x78). SDK 4's GLSLC needs its section headers read (Dark Souls Remastered in section 6).
|
- **GLSLC outputs in FromSoftware's dvdbnd archives** (Dark Souls Remastered): RomFS is two archives, `dvdbnd0.bdt` and `dvdbnd1.bdt` (3.6 GB), each indexed by the `.bhd5` beside it: `BHD5`, 0xff (little-endian), u32 1, u32 file size, u32 bucket count, u32 0, u64 bucket table offset; each 16-byte bucket `{u32 count, u32, u64 offset}` points at 16-byte entries `{u32 path hash, u32 size, u64 offset}` (paths only hashed). 5,184 entries are DCX containers (`DCX\0`, big-endian header, `DFLT` = a zlib stream at +0x4c), most of them BND3 archives (28-byte entries `{u8 flags, i32 size, u64 offset, i32 ID, u32 name offset, i32 unpacked size}` in formats 0x5c / 0x7c). Six BND3 archives in `dvdbnd0.bdt` hold the shaders, one GLSLC output per entry (`.vpo` vertex, `.fpo` pixel, named `Shader/FRPG_Sfx/NVNShaderBinary/ps/…` or by base name in the two FLVER material archives, 2,602 + 486 entries): `bhd5_entries`, `undcx`, `bnd3_members`, `bdt_shaders` in `shader_split.py`, in 64 jobs by entry range, about 10 s. Only archives that contain a GLSLC magic are parsed (the others use format 0x78). SDK 4's GLSLC needs its section headers read (Dark Souls Remastered in section 6).
|
||||||
- **BAHS** (agl `sharcb`, engine utility shaders): code region at header +0x1c (size +0x18). Binary entries in the table area are 16 bytes `{code offset, 0, control size (0x880/0x88c; 0x848 in BAHS version 9, Super Mario 3D World), signed offset from this entry to the control section}`. Control sections are shared between programs, so pair by entry, never by searching for code near a control.
|
- **BAHS** (agl `sharcb`, engine utility shaders): code region at header +0x1c (size +0x18). Binary entries in the table area are 16 bytes `{code offset, 0, control size (0x880/0x88c; 0x848 in BAHS version 9, Super Mario 3D World), signed offset from this entry to the control section}`. Control sections are shared between programs, so pair by entry, never by searching for code near a control.
|
||||||
- **Shorter control sections (SDK 10, Super Mario 3D World + Bowser's Fury)**: the control section stores its own length at +0x20. Mario's, Engage's, Dread's and 13 Sentinels' are all at least 0x880 bytes; 3D World has 2,888 of 0x850 bytes (its ShaderData bfsha and six layouts) and 0x848 in BAHS version 9, each followed directly by other data. The key therefore hashes the first `min(0x880, control +0x20)` bytes (`ctl_len` in `shader_split.py`, `keys_hash` in `hle_nvn.c`); the other games' tables came out byte-identical.
|
- **Shorter control sections (SDK 10, Super Mario 3D World + Bowser's Fury)**: the control section stores its own length at +0x20. Mario's, Engage's, Dread's and 13 Sentinels' are all at least 0x880 bytes; 3D World has 2,888 of 0x850 bytes (its ShaderData bfsha and six layouts) and 0x848 in BAHS version 9, each followed directly by other data. The key therefore hashes the first `min(0x880, control +0x20)` bytes (`ctl_len` in `shader_split.py`, `keys_hash` in `hle_nvn.c`); the other games' tables came out byte-identical.
|
||||||
|
|||||||
@@ -528,7 +528,7 @@ def survey(nsp_dir, app, fam, keys, repo, py, env):
|
|||||||
elif rom['mags']['PlatinumGames PKZ']: notes.append('PlatinumGames PKZ archives (pkzl; Bayonetta 3): DAT archives inside, compressed per entry (OodleKraken, ZStandard or None); the shaders are shader objects (VSO / PSO / CSO …) in DATs, which shader_split.py reads (pkz_entries, pg_shader), Oodle Kraken through the game\'s own streaming decoder in the Mac host (dm/host/hle_oodle.c)')
|
elif rom['mags']['PlatinumGames PKZ']: notes.append('PlatinumGames PKZ archives (pkzl; Bayonetta 3): DAT archives inside, compressed per entry (OodleKraken, ZStandard or None); the shaders are shader objects (VSO / PSO / CSO …) in DATs, which shader_split.py reads (pkz_entries, pg_shader), Oodle Kraken through the game\'s own streaming decoder in the Mac host (dm/host/hle_oodle.c)')
|
||||||
elif rom['mags']['MT Framework ARC']: notes.append('MT Framework ARC archives (Capcom, The Great Ace Attorney Chronicles): the shaders are in a shader package (SPK) inside one of them, which shader_split.py reads (mtarc_members, mtspk_shaders)')
|
elif rom['mags']['MT Framework ARC']: notes.append('MT Framework ARC archives (Capcom, The Great Ace Attorney Chronicles): the shaders are in a shader package (SPK) inside one of them, which shader_split.py reads (mtarc_members, mtspk_shaders)')
|
||||||
elif rom['mags']['Bezel Engine archive']: notes.append('NintendoWare Bezel Engine archives (.bea, magic SCNE; Big Brain Academy: Brain vs. Brain): every entry is a zstd frame, and the shaders are BNSH in their .bfsha entries, which shader_split.py reads (bea_members)')
|
elif rom['mags']['Bezel Engine archive']: notes.append('NintendoWare Bezel Engine archives (.bea, magic SCNE; Big Brain Academy: Brain vs. Brain): every entry is a zstd frame, and the shaders are BNSH in their .bfsha entries, which shader_split.py reads (bea_members)')
|
||||||
elif rom['mags']['XNB'] and 'MonoGame' in F['markers']: notes.append('MonoGame (XNB content): the shaders are MonoGame effects (MGFX version 8, NVN control sections and code areas) in the XNB files EffectReader reads, and SpriteBatch\'s in main, which shader_split.py reads (mgfx_effect; Celeste). A compressed XNB (LZX / LZ4) is not read')
|
elif rom['mags']['XNB'] and 'MonoGame' in F['markers']: notes.append('MonoGame (XNB content): the shaders are MonoGame effects (MGFX version 8, NVN control sections and code areas) in the XNB files EffectReader reads, and SpriteBatch\'s in main, which shader_split.py reads (mgfx_effect; Celeste), a compressed XNB (MonoGame\'s LZX or LZ4, Stardew Valley) after decompressing it (xnb_decompress)')
|
||||||
elif not (rom['mags']['BNSH'] or rom['mags']['BAHS']) and not rom['mags']['UnityFS'] and not vulkan and not gl and not F['glslc']:
|
elif not (rom['mags']['BNSH'] or rom['mags']['BAHS']) and not rom['mags']['UnityFS'] and not vulkan and not gl and not F['glslc']:
|
||||||
notes.append('no BNSH / BAHS at the top level: the shaders are inside compressed or other containers or in main, and must be located first')
|
notes.append('no BNSH / BAHS at the top level: the shaders are inside compressed or other containers or in main, and must be located first')
|
||||||
major = {v.split('_')[0] for v in F['sdkver']}
|
major = {v.split('_')[0] for v in F['sdkver']}
|
||||||
|
|||||||
Reference in new issue
Block a user