Decomp updates
This commit is contained in:
parent
f6a5155675
commit
c4fa8a6b05
62 changed files with 9413 additions and 20 deletions
314
_tmp_psx_gpu_search.py
Normal file
314
_tmp_psx_gpu_search.py
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import bisect
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(r"k:/ghidra/Crusader_Decomp")
|
||||
BUNDLE_DIR = ROOT / "out/psx_wdl/L0/sprite_bundles/bundle_000A1B04"
|
||||
FRAME_PATH = BUNDLE_DIR / "frame_000.bin"
|
||||
BUNDLE_JSON = BUNDLE_DIR / "bundle.json"
|
||||
GPU_PATH = ROOT / "binary/Crusader - No Remorse (USA) GPU RAM.bin"
|
||||
L0_WDL_PATH = Path(r"e:/emu/psx/Crusader - No Remorse/LSET1/L0.WDL")
|
||||
ROW_BYTES = 2048
|
||||
GPU_ROWS = 512
|
||||
TOP_N = 10
|
||||
FRAMEBUFFER_WIDTH = 320
|
||||
FRAMEBUFFER_HEIGHT = 240
|
||||
MATCH_TOP_N = 12
|
||||
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
|
||||
from psx_extract_wdl import colorize_indexed_pixels, psx_555_to_rgba, write_overview_grid, write_psx_16bpp_png
|
||||
|
||||
|
||||
def find_all(haystack: bytes, needle: bytes):
|
||||
start = 0
|
||||
while True:
|
||||
index = haystack.find(needle, start)
|
||||
if index < 0:
|
||||
return
|
||||
yield index
|
||||
start = index + 1
|
||||
|
||||
|
||||
def count_row_mismatches(left: bytes, right: bytes) -> int:
|
||||
return sum(a != b for a, b in zip(left, right))
|
||||
|
||||
|
||||
def is_exact_at(rows: list[bytes], candidate_rows: list[bytes], x: int, y: int, width: int) -> bool:
|
||||
for dy, src in enumerate(candidate_rows):
|
||||
if rows[y + dy][x : x + width] != src:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def near_score(
|
||||
rows: list[bytes],
|
||||
candidate_rows: list[bytes],
|
||||
x: int,
|
||||
y: int,
|
||||
width: int,
|
||||
cutoff: int | None,
|
||||
) -> tuple[int, list[int], bool]:
|
||||
total = 0
|
||||
row_mismatches: list[int] = []
|
||||
for dy, src in enumerate(candidate_rows):
|
||||
seg = rows[y + dy][x : x + width]
|
||||
mismatch = 0 if seg == src else count_row_mismatches(seg, src)
|
||||
total += mismatch
|
||||
row_mismatches.append(mismatch)
|
||||
if cutoff is not None and total > cutoff:
|
||||
return total, row_mismatches, True
|
||||
return total, row_mismatches, False
|
||||
|
||||
|
||||
def rgba_from_words(words: tuple[int, ...]) -> list[tuple[int, int, int]]:
|
||||
return [psx_555_to_rgba(word)[:3] for word in words]
|
||||
|
||||
|
||||
def candidate_match_score(
|
||||
framebuffer_rgb: list[tuple[int, int, int]],
|
||||
framebuffer_width: int,
|
||||
framebuffer_height: int,
|
||||
rgba: bytes,
|
||||
width: int,
|
||||
height: int,
|
||||
guess_x: int,
|
||||
guess_y: int,
|
||||
radius: int = 12,
|
||||
step: int = 2,
|
||||
) -> tuple[int, int, int]:
|
||||
best_score: int | None = None
|
||||
best_x = -1
|
||||
best_y = -1
|
||||
x_min = max(0, guess_x - radius)
|
||||
x_max = min(framebuffer_width - width, guess_x + radius)
|
||||
y_min = max(0, guess_y - radius)
|
||||
y_max = min(framebuffer_height - height, guess_y + radius)
|
||||
for y in range(y_min, y_max + 1):
|
||||
for x in range(x_min, x_max + 1):
|
||||
score = 0
|
||||
samples = 0
|
||||
for sy in range(0, height, step):
|
||||
screen_row = (y + sy) * framebuffer_width
|
||||
sprite_row = sy * width * 4
|
||||
for sx in range(0, width, step):
|
||||
src = sprite_row + sx * 4
|
||||
if rgba[src + 3] == 0:
|
||||
continue
|
||||
screen_r, screen_g, screen_b = framebuffer_rgb[screen_row + x + sx]
|
||||
red = rgba[src]
|
||||
green = rgba[src + 1]
|
||||
blue = rgba[src + 2]
|
||||
score += abs(screen_r - red) + abs(screen_g - green) + abs(screen_b - blue)
|
||||
samples += 1
|
||||
if samples == 0:
|
||||
continue
|
||||
normalized = score // samples
|
||||
if best_score is None or normalized < best_score:
|
||||
best_score = normalized
|
||||
best_x = x
|
||||
best_y = y
|
||||
if best_score is None:
|
||||
return 1 << 30, -1, -1
|
||||
return best_score, best_x, best_y
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bundle = json.loads(BUNDLE_JSON.read_text(encoding="ascii"))
|
||||
frame_meta = next(frame for frame in bundle["exported_frames"] if frame["index"] == 0)
|
||||
width = frame_meta["width"]
|
||||
height = frame_meta["height"]
|
||||
mode = bundle["mode"]
|
||||
frame = FRAME_PATH.read_bytes()
|
||||
expected = width * height
|
||||
if len(frame) != expected:
|
||||
raise SystemExit(f"frame byte size mismatch: got {len(frame)}, expected {expected}")
|
||||
if mode != 1:
|
||||
raise SystemExit(f"unexpected mode {mode}, expected 1 for 8bpp")
|
||||
|
||||
gpu = GPU_PATH.read_bytes()
|
||||
if len(gpu) != ROW_BYTES * GPU_ROWS:
|
||||
raise SystemExit(f"unexpected GPU dump size {len(gpu)}")
|
||||
|
||||
l0_data = L0_WDL_PATH.read_bytes()
|
||||
palette_offset = int.from_bytes(l0_data[8:12], "little")
|
||||
palette_size = int.from_bytes(l0_data[12:16], "little")
|
||||
if palette_size != 0x1000:
|
||||
raise SystemExit(f"unexpected palette size 0x{palette_size:X}")
|
||||
palette_blob = l0_data[palette_offset : palette_offset + palette_size]
|
||||
palettes_256 = [palette_blob[offset : offset + 0x200] for offset in range(0, len(palette_blob), 0x200)]
|
||||
|
||||
rows = [gpu[y * ROW_BYTES : (y + 1) * ROW_BYTES] for y in range(GPU_ROWS)]
|
||||
frame_rows = [frame[i * width : (i + 1) * width] for i in range(height)]
|
||||
flip_rows = [row[::-1] for row in frame_rows]
|
||||
|
||||
normal_hits: list[tuple[int, int]] = []
|
||||
flipped_hits: list[tuple[int, int]] = []
|
||||
for y in range(GPU_ROWS - height + 1):
|
||||
row = rows[y]
|
||||
normal_hits.extend((x, y) for x in find_all(row, frame_rows[0]))
|
||||
flipped_hits.extend((x, y) for x in find_all(row, flip_rows[0]))
|
||||
|
||||
exact_normal = [(x, y) for x, y in normal_hits if is_exact_at(rows, frame_rows, x, y, width)]
|
||||
exact_flipped = [(x, y) for x, y in flipped_hits if is_exact_at(rows, flip_rows, x, y, width)]
|
||||
|
||||
print(f"bundle_offset=0x{bundle['offset']:X} mode={mode} frame_count={bundle['frame_count']}")
|
||||
print(
|
||||
"frame0 "
|
||||
f"width={width} height={height} origin=({frame_meta['origin_x']},{frame_meta['origin_y']}) "
|
||||
f"data_start={frame_meta['data_start']} consumed={frame_meta['consumed']}"
|
||||
)
|
||||
print(f"frame_bytes={len(frame)} gpu_dump_bytes={len(gpu)}")
|
||||
print(f"row0_hits normal={len(normal_hits)} flipped={len(flipped_hits)}")
|
||||
print(f"exact_full_matches_normal={len(exact_normal)}")
|
||||
for x, y in exact_normal[:TOP_N]:
|
||||
print(f" normal x={x} y={y} page=({x // 256},{y // 256}) in_page=({x % 256},{y % 256})")
|
||||
print(f"exact_full_matches_flipped={len(exact_flipped)}")
|
||||
for x, y in exact_flipped[:TOP_N]:
|
||||
print(f" flipped x={x} y={y} page=({x // 256},{y // 256}) in_page=({x % 256},{y % 256})")
|
||||
|
||||
live_palette_entries: list[dict[str, object]] = []
|
||||
live_palette_labels: list[str] = []
|
||||
for clut_row in range(8):
|
||||
y = 0xF0 + clut_row
|
||||
row_words = struct.unpack("<1024H", rows[y])
|
||||
for column in range(16):
|
||||
x = column * 16
|
||||
palette = list(row_words[x : x + 256])
|
||||
rgba = colorize_indexed_pixels(frame, width, height, mode, palette)
|
||||
live_palette_entries.append(
|
||||
{
|
||||
"width": width,
|
||||
"height": height,
|
||||
"rgba": rgba,
|
||||
}
|
||||
)
|
||||
live_palette_labels.append(f"index={clut_row * 16 + column} x={x} y={y}")
|
||||
|
||||
atlas_path = BUNDLE_DIR / "live_vram_clut_atlas.png"
|
||||
labels_path = BUNDLE_DIR / "live_vram_clut_atlas.txt"
|
||||
write_overview_grid(atlas_path, live_palette_entries, columns=16)
|
||||
labels_path.write_text("\n".join(live_palette_labels) + "\n", encoding="ascii")
|
||||
print(f"live_vram_clut_atlas={atlas_path}")
|
||||
print(f"live_vram_clut_labels={labels_path}")
|
||||
|
||||
framebuffer_path = ROOT / "binary/psx_framebuffer_left.png"
|
||||
framebuffer_crop_path = ROOT / "binary/psx_framebuffer_console_crop.png"
|
||||
print(f"raw_palette_blocks_256={len(palettes_256)}")
|
||||
for palette_index, palette in enumerate(palettes_256):
|
||||
palette_hits: list[tuple[int, int]] = []
|
||||
for y in range(240, 256):
|
||||
row = rows[y]
|
||||
start = 0
|
||||
while True:
|
||||
x = row.find(palette, start)
|
||||
if x < 0:
|
||||
break
|
||||
palette_hits.append((x, y))
|
||||
start = x + 1
|
||||
print(f" palette_{palette_index}_hits={len(palette_hits)}")
|
||||
for x, y in palette_hits[:TOP_N]:
|
||||
print(f" palette_{palette_index} x={x} y={y} row_band={y - 240}")
|
||||
|
||||
framebuffer_bytes = bytearray(FRAMEBUFFER_WIDTH * FRAMEBUFFER_HEIGHT * 2)
|
||||
for y in range(FRAMEBUFFER_HEIGHT):
|
||||
src_row = rows[y]
|
||||
start = y * FRAMEBUFFER_WIDTH * 2
|
||||
framebuffer_bytes[start : start + FRAMEBUFFER_WIDTH * 2] = src_row[: FRAMEBUFFER_WIDTH * 2]
|
||||
write_psx_16bpp_png(framebuffer_path, bytes(framebuffer_bytes), FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT)
|
||||
framebuffer_words = struct.unpack(f"<{FRAMEBUFFER_WIDTH * FRAMEBUFFER_HEIGHT}H", bytes(framebuffer_bytes))
|
||||
framebuffer_rgb = rgba_from_words(framebuffer_words)
|
||||
|
||||
crop_x = 70
|
||||
crop_y = 0
|
||||
crop_width = 210
|
||||
crop_height = 110
|
||||
crop_bytes = bytearray(crop_width * crop_height * 2)
|
||||
for y in range(crop_height):
|
||||
src = rows[crop_y + y]
|
||||
src_start = crop_x * 2
|
||||
src_end = src_start + crop_width * 2
|
||||
dst_start = y * crop_width * 2
|
||||
crop_bytes[dst_start : dst_start + crop_width * 2] = src[src_start:src_end]
|
||||
write_psx_16bpp_png(framebuffer_crop_path, bytes(crop_bytes), crop_width, crop_height)
|
||||
print(f"framebuffer_left={framebuffer_path}")
|
||||
print(f"framebuffer_console_crop={framebuffer_crop_path}")
|
||||
|
||||
palette_rankings: list[tuple[int, int, int, int]] = []
|
||||
for palette_index, entry in enumerate(live_palette_entries):
|
||||
score, best_x, best_y = candidate_match_score(
|
||||
framebuffer_rgb,
|
||||
FRAMEBUFFER_WIDTH,
|
||||
FRAMEBUFFER_HEIGHT,
|
||||
entry["rgba"],
|
||||
width,
|
||||
height,
|
||||
guess_x=107,
|
||||
guess_y=12,
|
||||
)
|
||||
palette_rankings.append((score, palette_index, best_x, best_y))
|
||||
palette_rankings.sort()
|
||||
ranking_path = BUNDLE_DIR / "live_vram_clut_rank.txt"
|
||||
top_atlas_path = BUNDLE_DIR / "live_vram_clut_top_matches.png"
|
||||
best_candidate_path = BUNDLE_DIR / "live_vram_clut_best.png"
|
||||
ranking_lines = []
|
||||
print(f"best_live_vram_clut_matches_top_{MATCH_TOP_N}={min(MATCH_TOP_N, len(palette_rankings))}")
|
||||
top_entries: list[dict[str, object]] = []
|
||||
for score, palette_index, best_x, best_y in palette_rankings[:MATCH_TOP_N]:
|
||||
label = live_palette_labels[palette_index]
|
||||
line = f"score={score} {label} screen=({best_x},{best_y})"
|
||||
ranking_lines.append(line)
|
||||
print(f" {line}")
|
||||
top_entries.append(live_palette_entries[palette_index])
|
||||
ranking_path.write_text("\n".join(ranking_lines) + "\n", encoding="ascii")
|
||||
print(f"live_vram_clut_rank={ranking_path}")
|
||||
write_overview_grid(top_atlas_path, top_entries, columns=4)
|
||||
print(f"live_vram_clut_top_matches={top_atlas_path}")
|
||||
if palette_rankings:
|
||||
best_palette_index = palette_rankings[0][1]
|
||||
best_entry = live_palette_entries[best_palette_index]
|
||||
write_overview_grid(best_candidate_path, [best_entry], columns=1)
|
||||
print(f"live_vram_clut_best={best_candidate_path}")
|
||||
|
||||
if exact_normal or exact_flipped:
|
||||
return
|
||||
|
||||
ranked: list[tuple[int, int, int, str, list[int]]] = []
|
||||
cutoff: int | None = None
|
||||
for orientation, hits, candidate_rows in (
|
||||
("normal", normal_hits, frame_rows),
|
||||
("flipped", flipped_hits, flip_rows),
|
||||
):
|
||||
for x, y in hits:
|
||||
total, row_mismatches, pruned = near_score(rows, candidate_rows, x, y, width, cutoff)
|
||||
if pruned and len(ranked) >= TOP_N and total > ranked[-1][0]:
|
||||
continue
|
||||
entry = (total, y, x, orientation, row_mismatches)
|
||||
insert_at = bisect.bisect_left(ranked, entry)
|
||||
ranked.insert(insert_at, entry)
|
||||
if len(ranked) > TOP_N:
|
||||
ranked.pop()
|
||||
if len(ranked) == TOP_N:
|
||||
cutoff = ranked[-1][0]
|
||||
|
||||
print(f"best_near_matches_top_{TOP_N}={len(ranked)}")
|
||||
for total, y, x, orientation, row_mismatches in ranked:
|
||||
nonzero_rows = [(index, mismatch) for index, mismatch in enumerate(row_mismatches) if mismatch]
|
||||
sample = ", ".join(f"r{index}={mismatch}" for index, mismatch in nonzero_rows[:8])
|
||||
if len(nonzero_rows) > 8:
|
||||
sample += ", ..."
|
||||
if not sample:
|
||||
sample = "all rows exact"
|
||||
print(
|
||||
f" {orientation} x={x} y={y} page=({x // 256},{y // 256}) in_page=({x % 256},{y % 256}) "
|
||||
f"mismatches={total} details=[{sample}]"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue