add Linux diagnostics and texture preview tools
This commit is contained in:
parent
943d26e2d9
commit
0662d0a92d
6 changed files with 640 additions and 56 deletions
|
|
@ -36,6 +36,7 @@ vblank_precise_spin_us = 150
|
|||
host_present_from_non_ui_thread = true
|
||||
presenter_strict_guest_output_backpressure = false
|
||||
presenter_strict_guest_output_backpressure_timeout_ms = 100
|
||||
vulkan_async_placeholder_max_consecutive_skips = 120
|
||||
|
||||
present_letterbox = true
|
||||
present_safe_area_x = 100
|
||||
|
|
|
|||
212
scripts/decode_xenos_texture_dumps.py
Executable file
212
scripts/decode_xenos_texture_dumps.py
Executable file
|
|
@ -0,0 +1,212 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Decode debug-dumped Xbox 360 tiled RGBA8 resolve textures to PNGs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as exc:
|
||||
raise SystemExit("Pillow is required to write PNGs: python3 -m pip install --user pillow") from exc
|
||||
|
||||
|
||||
DUMP_NAME_RE = re.compile(r"addr_([0-9a-fA-F]{8})_len_([0-9a-fA-F]+)_")
|
||||
|
||||
# These are the render-target resolve ranges observed around the Import Skater
|
||||
# preset-preview path. Pitch is in pixels, including Xbox 360 tile padding.
|
||||
KNOWN_SURFACES_BY_RANGE = {
|
||||
(0x04614000, 0x02D000): (288, 160, 288),
|
||||
(0x0460E000, 0x006000): (72, 40, 96),
|
||||
(0x0460A000, 0x001000): (32, 16, 32),
|
||||
(0x0460B000, 0x001000): (32, 16, 32),
|
||||
(0x04911000, 0x2D0000): (1152, 640, 1152),
|
||||
(0x04641000, 0x2D0000): (1152, 640, 1152),
|
||||
(0x04BE1000, 0x2D0000): (1152, 640, 1152),
|
||||
(0x04EB1000, 0x2D0000): (1152, 640, 1152),
|
||||
(0x04BE1000, 0x168000): (1152, 320, 1152),
|
||||
(0x04D49000, 0x168000): (1152, 320, 1152),
|
||||
(0x04EB1000, 0x168000): (1152, 320, 1152),
|
||||
(0x05019000, 0x168000): (1152, 320, 1152),
|
||||
(0x05258000, 0x398000): (1280, 720, 1280),
|
||||
(0x03554000, 0x300000): (1024, 768, 1024),
|
||||
(0x03954000, 0x300000): (1024, 768, 1024),
|
||||
(0x03C54000, 0x100000): (512, 512, 512),
|
||||
}
|
||||
|
||||
KNOWN_SURFACES_BY_LENGTH = {
|
||||
0x02D000: (288, 160, 288),
|
||||
0x006000: (72, 40, 96),
|
||||
0x001000: (32, 16, 32),
|
||||
0x2D0000: (1152, 640, 1152),
|
||||
0x168000: (1152, 320, 1152),
|
||||
0x398000: (1280, 720, 1280),
|
||||
0x384000: (1280, 720, 1280),
|
||||
0x300000: (1024, 768, 1024),
|
||||
0x100000: (512, 512, 512),
|
||||
}
|
||||
|
||||
|
||||
def tiled_offset_2d_row(y: int, pitch: int, log2_bpp: int) -> int:
|
||||
macro = ((y // 32) * (pitch // 32)) << (log2_bpp + 7)
|
||||
micro = ((y & 6) << 2) << log2_bpp
|
||||
return (
|
||||
macro
|
||||
+ ((micro & ~0xF) << 1)
|
||||
+ (micro & 0xF)
|
||||
+ ((y & 8) << (3 + log2_bpp))
|
||||
+ ((y & 1) << 4)
|
||||
)
|
||||
|
||||
|
||||
def tiled_offset_2d_column(x: int, y: int, log2_bpp: int, base_offset: int) -> int:
|
||||
macro = (x // 32) << (log2_bpp + 7)
|
||||
micro = (x & 7) << log2_bpp
|
||||
offset = base_offset + (macro + ((micro & ~0xF) << 1) + (micro & 0xF))
|
||||
return (
|
||||
((offset & ~0x1FF) << 3)
|
||||
+ ((offset & 0x1C0) << 2)
|
||||
+ (offset & 0x3F)
|
||||
+ ((y & 16) << 7)
|
||||
+ (((((y & 8) >> 2) + (x >> 3)) & 3) << 6)
|
||||
)
|
||||
|
||||
|
||||
def parse_dump_name(path: Path) -> tuple[int, int] | None:
|
||||
match = DUMP_NAME_RE.search(path.name)
|
||||
if not match:
|
||||
return None
|
||||
return int(match.group(1), 16), int(match.group(2), 16)
|
||||
|
||||
|
||||
def infer_surface(address: int, length: int) -> tuple[int, int, int] | None:
|
||||
surface = KNOWN_SURFACES_BY_RANGE.get((address, length))
|
||||
if surface:
|
||||
return surface
|
||||
surface = KNOWN_SURFACES_BY_LENGTH.get(length)
|
||||
if surface:
|
||||
return surface
|
||||
|
||||
pixels = length // 4
|
||||
for pitch in (1280, 1152, 1024, 768, 720, 640, 512, 384, 320, 288, 256, 128, 96, 64, 32):
|
||||
if pixels % pitch == 0:
|
||||
height = pixels // pitch
|
||||
if height > 0:
|
||||
return pitch, height, pitch
|
||||
return None
|
||||
|
||||
|
||||
def untile_rgba8(raw: bytes, width: int, height: int, pitch: int) -> bytearray:
|
||||
output = bytearray(width * height * 4)
|
||||
for y in range(height):
|
||||
row_offset = tiled_offset_2d_row(y, pitch, 2)
|
||||
for x in range(width):
|
||||
input_offset = tiled_offset_2d_column(x, y, 2, row_offset)
|
||||
input_offset = (input_offset >> 2) * 4
|
||||
if input_offset + 4 > len(raw):
|
||||
continue
|
||||
output_offset = (y * width + x) * 4
|
||||
output[output_offset : output_offset + 4] = raw[input_offset : input_offset + 4]
|
||||
return output
|
||||
|
||||
|
||||
def save_variants(untiled: bytearray, width: int, height: int, output_stem: Path) -> list[Path]:
|
||||
rgba = Image.frombytes("RGBA", (width, height), bytes(untiled))
|
||||
|
||||
bgra_bytes = bytearray(len(untiled))
|
||||
for i in range(0, len(untiled), 4):
|
||||
bgra_bytes[i + 0] = untiled[i + 2]
|
||||
bgra_bytes[i + 1] = untiled[i + 1]
|
||||
bgra_bytes[i + 2] = untiled[i + 0]
|
||||
bgra_bytes[i + 3] = untiled[i + 3]
|
||||
bgra = Image.frombytes("RGBA", (width, height), bytes(bgra_bytes))
|
||||
|
||||
alpha_rgb = bytearray(untiled)
|
||||
for i in range(3, len(alpha_rgb), 4):
|
||||
alpha_rgb[i] = 255
|
||||
rgba_opaque = Image.frombytes("RGBA", (width, height), bytes(alpha_rgb))
|
||||
|
||||
paths = [
|
||||
output_stem.with_name(output_stem.name + "_rgba.png"),
|
||||
output_stem.with_name(output_stem.name + "_bgra.png"),
|
||||
output_stem.with_name(output_stem.name + "_rgba_opaque.png"),
|
||||
]
|
||||
rgba.save(paths[0])
|
||||
bgra.save(paths[1])
|
||||
rgba_opaque.save(paths[2])
|
||||
return paths
|
||||
|
||||
|
||||
def write_index(rows: list[tuple[Path, int, int, int, int, list[Path]]], output_dir: Path) -> None:
|
||||
index_path = output_dir / "index.html"
|
||||
parts = [
|
||||
"<!doctype html><meta charset='utf-8'><title>Texture dumps</title>",
|
||||
"<style>body{font-family:sans-serif;margin:24px;background:#111;color:#eee}"
|
||||
"section{margin:0 0 32px}img{max-width:360px;image-rendering:auto;background:#444}"
|
||||
".row{display:flex;gap:16px;flex-wrap:wrap}.item{max-width:380px}"
|
||||
"code{color:#b7f7ff}</style>",
|
||||
"<h1>Texture dumps</h1>",
|
||||
]
|
||||
for raw_path, address, length, width, height, images in rows:
|
||||
parts.append(
|
||||
f"<section><h2><code>{html.escape(raw_path.name)}</code></h2>"
|
||||
f"<p>address <code>0x{address:08X}</code>, length <code>0x{length:X}</code>, "
|
||||
f"{width}x{height}</p><div class='row'>"
|
||||
)
|
||||
for image_path in images:
|
||||
label = image_path.name.removeprefix(raw_path.stem + "_").removesuffix(".png")
|
||||
parts.append(
|
||||
f"<div class='item'><p>{html.escape(label)}</p>"
|
||||
f"<img src='{html.escape(image_path.name)}'></div>"
|
||||
)
|
||||
parts.append("</div></section>")
|
||||
index_path.write_text("\n".join(parts), encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"input_dir",
|
||||
nargs="?",
|
||||
default="/home/alex/Desktop/skate_fresh/logs/texture_dumps/raw",
|
||||
help="Directory containing resolve_*.bin dumps",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default="/home/alex/Desktop/skate_fresh/logs/texture_dumps/rendered",
|
||||
help="Directory to write decoded PNGs and index.html",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
input_dir = Path(args.input_dir)
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
rows: list[tuple[Path, int, int, int, int, list[Path]]] = []
|
||||
for raw_path in sorted(input_dir.glob("resolve_*.bin")):
|
||||
parsed = parse_dump_name(raw_path)
|
||||
if not parsed:
|
||||
continue
|
||||
address, length = parsed
|
||||
surface = infer_surface(address, length)
|
||||
if not surface:
|
||||
print(f"Skipping {raw_path.name}: unknown dimensions for length 0x{length:X}")
|
||||
continue
|
||||
width, height, pitch = surface
|
||||
raw = raw_path.read_bytes()
|
||||
untiled = untile_rgba8(raw, width, height, pitch)
|
||||
output_stem = output_dir / raw_path.stem
|
||||
images = save_variants(untiled, width, height, output_stem)
|
||||
rows.append((raw_path, address, length, width, height, images))
|
||||
print(f"Decoded {raw_path.name}: {width}x{height}, pitch {pitch}")
|
||||
|
||||
write_index(rows, output_dir)
|
||||
print(f"Wrote {output_dir / 'index.html'}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
317
scripts/render_static_rx2_textures.py
Executable file
317
scripts/render_static_rx2_textures.py
Executable file
|
|
@ -0,0 +1,317 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Extract Skate 3 EB archives and render top-mip RX2 textures to PNG."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import math
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
TEXTURE_FORMATS = {
|
||||
0x52: ("DXT1", 8),
|
||||
0x53: ("DXT3", 16),
|
||||
0x54: ("DXT5", 16),
|
||||
}
|
||||
|
||||
|
||||
def safe_output_stem(path: Path, root: Path | None = None) -> str:
|
||||
try:
|
||||
label = path.relative_to(root) if root else path
|
||||
except ValueError:
|
||||
label = path
|
||||
return "__".join(part.replace("/", "_").replace("\\", "_") for part in label.with_suffix("").parts)
|
||||
|
||||
|
||||
def tiled_offset_2d_row(y: int, width: int, log2_bpp: int) -> int:
|
||||
macro = ((y // 32) * (width // 32)) << (log2_bpp + 7)
|
||||
micro = ((y & 6) << 2) << log2_bpp
|
||||
return (
|
||||
macro
|
||||
+ ((micro & ~0xF) << 1)
|
||||
+ (micro & 0xF)
|
||||
+ ((y & 8) << (3 + log2_bpp))
|
||||
+ ((y & 1) << 4)
|
||||
)
|
||||
|
||||
|
||||
def tiled_offset_2d_column(x: int, y: int, log2_bpp: int, base_offset: int) -> int:
|
||||
macro = (x // 32) << (log2_bpp + 7)
|
||||
micro = (x & 7) << log2_bpp
|
||||
offset = base_offset + (macro + ((micro & ~0xF) << 1) + (micro & 0xF))
|
||||
return (
|
||||
((offset & ~0x1FF) << 3)
|
||||
+ ((offset & 0x1C0) << 2)
|
||||
+ (offset & 0x3F)
|
||||
+ ((y & 16) << 7)
|
||||
+ (((((y & 8) >> 2) + (x >> 3)) & 3) << 6)
|
||||
)
|
||||
|
||||
|
||||
def swap16(data: bytes) -> bytes:
|
||||
output = bytearray(data)
|
||||
for offset in range(0, len(output) - 1, 2):
|
||||
output[offset], output[offset + 1] = output[offset + 1], output[offset]
|
||||
return bytes(output)
|
||||
|
||||
|
||||
def untile_blocks(data: bytes, blocks_w: int, blocks_h: int, bytes_per_block: int) -> bytes:
|
||||
log2_bpp = 3 if bytes_per_block == 8 else 4
|
||||
pitch_blocks = max(32, math.ceil(blocks_w / 32) * 32)
|
||||
output = bytearray(blocks_w * blocks_h * bytes_per_block)
|
||||
for y in range(blocks_h):
|
||||
row_offset = tiled_offset_2d_row(y, pitch_blocks, log2_bpp)
|
||||
for x in range(blocks_w):
|
||||
input_offset = tiled_offset_2d_column(x, y, log2_bpp, row_offset)
|
||||
input_offset = (input_offset >> log2_bpp) * bytes_per_block
|
||||
output_offset = (y * blocks_w + x) * bytes_per_block
|
||||
if input_offset + bytes_per_block <= len(data):
|
||||
output[output_offset : output_offset + bytes_per_block] = data[
|
||||
input_offset : input_offset + bytes_per_block
|
||||
]
|
||||
return swap16(output)
|
||||
|
||||
|
||||
def make_dds(width: int, height: int, fourcc: str, pitch_or_size: int, payload: bytes) -> bytes:
|
||||
header = bytearray(b"DDS ")
|
||||
header += struct.pack("<I", 124)
|
||||
header += struct.pack("<I", 0x0002100F)
|
||||
header += struct.pack("<I", height)
|
||||
header += struct.pack("<I", width)
|
||||
header += struct.pack("<I", pitch_or_size)
|
||||
header += struct.pack("<I", 0)
|
||||
header += struct.pack("<I", 1)
|
||||
header += bytes(44)
|
||||
header += struct.pack("<I", 32)
|
||||
header += struct.pack("<I", 0x4)
|
||||
header += fourcc.encode("ascii")
|
||||
header += struct.pack("<I", 0)
|
||||
header += struct.pack("<I", 0)
|
||||
header += struct.pack("<I", 0)
|
||||
header += struct.pack("<I", 0)
|
||||
header += struct.pack("<I", 0)
|
||||
header += struct.pack("<I", 0x1000)
|
||||
header += struct.pack("<I", 0)
|
||||
header += struct.pack("<I", 0)
|
||||
header += struct.pack("<I", 0)
|
||||
header += struct.pack("<I", 0)
|
||||
return bytes(header) + payload
|
||||
|
||||
|
||||
def parse_rx2_header(data: bytes) -> tuple[int, int, int, int] | None:
|
||||
if not data.startswith(b"\x89RW4xb2") or len(data) < 0x238:
|
||||
return None
|
||||
data_offset = int.from_bytes(data[0x44:0x48], "big")
|
||||
if data_offset <= 0 or data_offset >= len(data):
|
||||
data_offset = 0x238
|
||||
fmt = data[0x167]
|
||||
height = (int.from_bytes(data[0x168:0x16A], "big") + 1) * 8
|
||||
width = (int.from_bytes(data[0x16A:0x16C], "big") + 1) & 0x1FFF
|
||||
if width <= 0 or height <= 0 or width > 8192 or height > 8192:
|
||||
return None
|
||||
return fmt, width, height, data_offset
|
||||
|
||||
|
||||
def render_rx2(
|
||||
path: Path,
|
||||
output_dir: Path,
|
||||
root: Path | None = None,
|
||||
layout: str = "tiled",
|
||||
) -> Path | None:
|
||||
output_path = output_dir / "images" / (safe_output_stem(path, root) + ".png")
|
||||
if output_path.exists():
|
||||
return output_path
|
||||
|
||||
data = path.read_bytes()
|
||||
parsed = parse_rx2_header(data)
|
||||
if not parsed:
|
||||
return None
|
||||
fmt, width, height, data_offset = parsed
|
||||
format_info = TEXTURE_FORMATS.get(fmt)
|
||||
if not format_info:
|
||||
return None
|
||||
|
||||
fourcc, bytes_per_block = format_info
|
||||
blocks_w = max(1, math.ceil(width / 4))
|
||||
blocks_h = max(1, math.ceil(height / 4))
|
||||
top_mip_size = blocks_w * blocks_h * bytes_per_block
|
||||
payload = data[data_offset : data_offset + top_mip_size]
|
||||
if len(payload) < top_mip_size:
|
||||
return None
|
||||
|
||||
if layout == "linear":
|
||||
decoded = swap16(payload)
|
||||
elif layout == "tiled":
|
||||
decoded = untile_blocks(payload, blocks_w, blocks_h, bytes_per_block)
|
||||
else:
|
||||
raise ValueError(f"Unknown RX2 layout: {layout}")
|
||||
|
||||
dds = make_dds(width, height, fourcc, top_mip_size, decoded)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
Image.open(__import__("io").BytesIO(dds)).convert("RGBA").save(output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
def write_texture_page(
|
||||
rendered: list[tuple[Path, Path]],
|
||||
output_dir: Path,
|
||||
page_number: int,
|
||||
page_count: int,
|
||||
) -> str:
|
||||
page_name = f"page-{page_number:03d}.html"
|
||||
rows = [
|
||||
"<!doctype html><meta charset='utf-8'><title>Skate 3 RX2 textures</title>",
|
||||
"<style>body{font-family:sans-serif;background:#111;color:#eee;margin:24px}"
|
||||
".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:14px}"
|
||||
".item{background:#1b1b1b;padding:8px;border-radius:4px;overflow:hidden}"
|
||||
"img{width:100%;height:auto;max-height:240px;object-fit:contain;background:#333;image-rendering:auto}"
|
||||
"code{font-size:11px;word-break:break-all;color:#cceeff}"
|
||||
"a{color:#9bdcff}.nav{display:flex;gap:12px;align-items:center;margin:12px 0 20px}</style>",
|
||||
f"<h1>Skate 3 RX2 Textures: Page {page_number} / {page_count}</h1>",
|
||||
"<div class='nav'>",
|
||||
"<a href='index.html'>Index</a>",
|
||||
]
|
||||
if page_number > 1:
|
||||
rows.append(f"<a href='page-{page_number - 1:03d}.html'>Previous</a>")
|
||||
if page_number < page_count:
|
||||
rows.append(f"<a href='page-{page_number + 1:03d}.html'>Next</a>")
|
||||
rows.append("</div><div class='grid'>")
|
||||
for source, image in rendered:
|
||||
rel_image = image.relative_to(output_dir)
|
||||
rows.append(
|
||||
f"<div class='item'><img loading='lazy' src='{html.escape(str(rel_image))}'>"
|
||||
f"<p><code>{html.escape(source.name)}</code></p></div>"
|
||||
)
|
||||
rows.append("</div>")
|
||||
(output_dir / page_name).write_text("\n".join(rows), encoding="utf-8")
|
||||
return page_name
|
||||
|
||||
|
||||
def write_index(rendered: list[tuple[Path, Path]], output_dir: Path, page_size: int) -> None:
|
||||
page_size = max(1, page_size)
|
||||
pages = [rendered[offset : offset + page_size] for offset in range(0, len(rendered), page_size)]
|
||||
page_names = [
|
||||
write_texture_page(page, output_dir, page_number, len(pages))
|
||||
for page_number, page in enumerate(pages, start=1)
|
||||
]
|
||||
|
||||
rows = [
|
||||
"<!doctype html><meta charset='utf-8'><title>Skate 3 RX2 textures</title>",
|
||||
"<style>body{font-family:sans-serif;background:#111;color:#eee;margin:24px}"
|
||||
"a{color:#9bdcff}.pages{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px}"
|
||||
".page{background:#1b1b1b;padding:12px;border-radius:4px}</style>",
|
||||
"<h1>Skate 3 RX2 Textures</h1>",
|
||||
f"<p>{len(rendered)} textures rendered. Open one page at a time to avoid loading every PNG at once.</p>",
|
||||
"<div class='pages'>",
|
||||
]
|
||||
for index, page_name in enumerate(page_names, start=1):
|
||||
start = ((index - 1) * page_size) + 1
|
||||
end = min(index * page_size, len(rendered))
|
||||
rows.append(
|
||||
f"<a class='page' href='{page_name}'>Page {index}<br>{start}-{end}</a>"
|
||||
)
|
||||
rows.append("</div>")
|
||||
(output_dir / "index.html").write_text("\n".join(rows), encoding="utf-8")
|
||||
|
||||
|
||||
def maybe_extract_big(args: argparse.Namespace) -> Path:
|
||||
extracted_dir = Path(args.extracted_dir)
|
||||
texture_dir = extracted_dir / "data/content/createacharacter/texture"
|
||||
if texture_dir.exists() and list(texture_dir.glob("*.rx2")):
|
||||
return extracted_dir
|
||||
|
||||
if not args.big:
|
||||
raise SystemExit("No extracted textures found and --big was not provided.")
|
||||
quickbms = shutil.which(args.quickbms)
|
||||
if not quickbms:
|
||||
raise SystemExit(f"QuickBMS executable not found: {args.quickbms}")
|
||||
script = Path(args.bms_script)
|
||||
if not script.exists():
|
||||
raise SystemExit(f"BMS script not found: {script}")
|
||||
|
||||
extracted_dir.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run([quickbms, "-Y", str(script), str(args.big), str(extracted_dir)], check=True)
|
||||
return extracted_dir
|
||||
|
||||
|
||||
def read_paths_file(path: Path) -> list[Path]:
|
||||
files: list[Path] = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
files.append(Path(line))
|
||||
return files
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--big", type=Path, help="Optional createacharacter.big path to extract first")
|
||||
parser.add_argument("--quickbms", default="/tmp/quickbms_skate/quickbms_linux/quickbms")
|
||||
parser.add_argument("--bms-script", default="/tmp/quickbms_skate/fightnight.bms")
|
||||
parser.add_argument("--extracted-dir", default="/tmp/skate_static_rx2")
|
||||
parser.add_argument(
|
||||
"--texture-dir",
|
||||
type=Path,
|
||||
help="Directory containing RX2 files to render. Defaults to createacharacter/texture.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--paths-file",
|
||||
type=Path,
|
||||
help="Text file containing explicit RX2 paths to render, one per line.",
|
||||
)
|
||||
parser.add_argument("--recursive", action="store_true", help="Recursively scan --texture-dir")
|
||||
parser.add_argument(
|
||||
"--layout",
|
||||
choices=("tiled", "linear"),
|
||||
default="tiled",
|
||||
help="RX2 memory layout. createacharacter textures are tiled; FE/menu textures are usually linear.",
|
||||
)
|
||||
parser.add_argument("--output-dir", default="/tmp/skate_static_rx2_rendered")
|
||||
parser.add_argument("--limit", type=int, default=500, help="Maximum textures to render")
|
||||
parser.add_argument("--page-size", type=int, default=100, help="Textures per HTML page")
|
||||
parser.add_argument("--verbose", action="store_true", help="Print every rendered texture")
|
||||
args = parser.parse_args()
|
||||
|
||||
extracted_dir = Path(args.extracted_dir)
|
||||
if args.paths_file:
|
||||
sources = read_paths_file(args.paths_file)
|
||||
source_root = None
|
||||
else:
|
||||
if args.texture_dir:
|
||||
texture_dir = args.texture_dir
|
||||
source_root = texture_dir
|
||||
else:
|
||||
extracted_dir = maybe_extract_big(args)
|
||||
texture_dir = extracted_dir / "data/content/createacharacter/texture"
|
||||
source_root = texture_dir
|
||||
globber = texture_dir.rglob if args.recursive else texture_dir.glob
|
||||
sources = sorted(globber("*.rx2"))
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
rendered: list[tuple[Path, Path]] = []
|
||||
for source in sources:
|
||||
image = render_rx2(source, output_dir, source_root, args.layout)
|
||||
if image:
|
||||
rendered.append((source, image))
|
||||
if args.verbose:
|
||||
print(f"Rendered {source.name}")
|
||||
if args.limit and len(rendered) >= args.limit:
|
||||
break
|
||||
|
||||
write_index(rendered, output_dir, args.page_size)
|
||||
print(f"Wrote {len(rendered)} textures to {output_dir}")
|
||||
print(f"Open {output_dir / 'index.html'}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -37,7 +37,6 @@
|
|||
#include <rex/filesystem.h>
|
||||
#include <rex/filesystem/devices/host_path_device.h>
|
||||
#include <rex/filesystem/vfs.h>
|
||||
#include <rex/graphics/graphics_system.h>
|
||||
#include <rex/input/input_system.h>
|
||||
#include <rex/kernel/xam/module.h>
|
||||
#include <rex/logging.h>
|
||||
|
|
@ -185,19 +184,24 @@ bool ConfigContainsAnyKey(const toml::table& table, std::initializer_list<std::s
|
|||
return false;
|
||||
}
|
||||
|
||||
bool DeveloperConfigHasResolutionScaleOverride(const std::filesystem::path& config_path) {
|
||||
bool ConfigFileContainsAnyKey(const std::filesystem::path& config_path,
|
||||
std::initializer_list<std::string_view> keys) {
|
||||
if (config_path.empty() || !std::filesystem::exists(config_path)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
auto config = toml::parse_file(config_path.string());
|
||||
return ConfigContainsAnyKey(config, {"resolution_scale", "draw_resolution_scale_x",
|
||||
"draw_resolution_scale_y"});
|
||||
return ConfigContainsAnyKey(config, keys);
|
||||
} catch (const toml::parse_error&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool DeveloperConfigHasResolutionScaleOverride(const std::filesystem::path& config_path) {
|
||||
return ConfigFileContainsAnyKey(config_path, {"resolution_scale", "draw_resolution_scale_x",
|
||||
"draw_resolution_scale_y"});
|
||||
}
|
||||
|
||||
constexpr int kDefaultResolutionScale = 2;
|
||||
|
||||
void ApplyFirstRunVideoDefaults(const std::filesystem::path& settings_path,
|
||||
|
|
@ -369,6 +373,14 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) {
|
|||
"Save draw fingerprint log", [this] {
|
||||
SaveDrawFingerprintLog();
|
||||
});
|
||||
rex::ui::RegisterBind("bind_skate3_log_debug_marker", "F9",
|
||||
"Write debug marker to log", [this] {
|
||||
LogDebugMarker();
|
||||
});
|
||||
rex::ui::RegisterBind("bind_skate3_log_user_marker", "F10",
|
||||
"Write marker to log", [this] {
|
||||
LogUserMarker();
|
||||
});
|
||||
}
|
||||
|
||||
void Skate3BaseApp::OnPostSetup() {
|
||||
|
|
@ -410,48 +422,14 @@ void Skate3BaseApp::OnPostSetup() {
|
|||
}
|
||||
}
|
||||
|
||||
void Skate3BaseApp::OnPostLaunchModule(rex::system::XThread* thread) {
|
||||
(void)thread;
|
||||
|
||||
if (std::getenv("SKATE3_INVALIDATE_GPU_MEMORY_EVERY_FRAME") == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* graphics_system =
|
||||
static_cast<rex::graphics::GraphicsSystem*>(runtime()->graphics_system());
|
||||
if (!graphics_system) {
|
||||
return;
|
||||
}
|
||||
|
||||
invalidate_gpu_memory_thread_running_.store(true, std::memory_order_release);
|
||||
invalidate_gpu_memory_thread_ = std::thread([this, graphics_system]() {
|
||||
while (invalidate_gpu_memory_thread_running_.load(
|
||||
std::memory_order_acquire)) {
|
||||
graphics_system->InvalidateGpuMemory();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Skate3BaseApp::OnShutdown() {
|
||||
rex::ui::UnregisterBind("bind_skate3_menu");
|
||||
rex::ui::UnregisterBind("bind_skate3_menu_alt");
|
||||
rex::ui::UnregisterBind("bind_skate3_save_draw_fingerprints");
|
||||
rex::ui::UnregisterBind("bind_skate3_log_debug_marker");
|
||||
rex::ui::UnregisterBind("bind_skate3_log_user_marker");
|
||||
ApplyGameplayCursorMode();
|
||||
simple_settings_dialog_.reset();
|
||||
StopGpuMemoryInvalidationThread();
|
||||
}
|
||||
|
||||
void Skate3BaseApp::OnGuestThreadExit(rex::system::XThread* thread) {
|
||||
(void)thread;
|
||||
StopGpuMemoryInvalidationThread();
|
||||
}
|
||||
|
||||
void Skate3BaseApp::StopGpuMemoryInvalidationThread() {
|
||||
invalidate_gpu_memory_thread_running_.store(false, std::memory_order_release);
|
||||
if (invalidate_gpu_memory_thread_.joinable()) {
|
||||
invalidate_gpu_memory_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
void Skate3BaseApp::ToggleSimpleSettings() {
|
||||
|
|
@ -501,10 +479,10 @@ void Skate3BaseApp::ToggleSimpleSettings() {
|
|||
};
|
||||
auto close_settings = [this]() { ApplyGameplayCursorMode(); };
|
||||
auto close_game = [this]() {
|
||||
#if REX_PLATFORM_MAC
|
||||
#if REX_PLATFORM_MAC || REX_PLATFORM_LINUX
|
||||
std::thread([]() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(10));
|
||||
REXLOG_WARN("macOS Close Game watchdog exiting process after shutdown timeout");
|
||||
REXLOG_WARN("Close Game watchdog exiting process after shutdown timeout");
|
||||
std::_Exit(EXIT_SUCCESS);
|
||||
}).detach();
|
||||
#endif
|
||||
|
|
@ -648,6 +626,28 @@ void Skate3BaseApp::SaveDrawFingerprintLog() {
|
|||
#endif
|
||||
}
|
||||
|
||||
void Skate3BaseApp::LogUserMarker() {
|
||||
const uint32_t marker = debug_marker_count_.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
REXLOG_WARN("USER LOG MARKER #{}: F10 pressed", marker);
|
||||
}
|
||||
|
||||
void Skate3BaseApp::LogDebugMarker() {
|
||||
const uint32_t marker = debug_marker_count_.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
rex::cvar::SetFlagByName("vulkan_debug_log_frame_summaries_remaining", "36000");
|
||||
rex::cvar::SetFlagByName("vulkan_debug_frame_summary_interval_frames", "300");
|
||||
rex::cvar::SetFlagByName("vulkan_debug_log_resolve_decisions_remaining", "10000");
|
||||
rex::cvar::SetFlagByName("vulkan_debug_log_team_profile_background_candidates_remaining",
|
||||
"1000");
|
||||
rex::cvar::SetFlagByName("vulkan_debug_log_team_profile_background_bindings_remaining",
|
||||
"256");
|
||||
rex::cvar::SetFlagByName("filesystem_debug_log_fe_asset_ops_remaining", "10000");
|
||||
rex::cvar::SetFlagByName("filesystem_debug_log_team_profile_background_remaining", "200");
|
||||
REXLOG_WARN(
|
||||
"USER DEBUG MARKER #{}: F9 pressed; enabled Vulkan frame summaries, resolve diagnostics, "
|
||||
"FE asset diagnostics, and team_profile_background_0 tracing",
|
||||
marker);
|
||||
}
|
||||
|
||||
void Skate3BaseApp::ApplySelectedProfileToRuntime() {
|
||||
if (!runtime() || !runtime()->kernel_state() || !runtime()->kernel_state()->user_profile()) {
|
||||
return;
|
||||
|
|
@ -703,51 +703,106 @@ std::set<std::string> Skate3BaseApp::DiscoverRecipeAliases(
|
|||
return aliases;
|
||||
}
|
||||
|
||||
bool Skate3BaseApp::CreateOverlayDirectory(
|
||||
const std::filesystem::path& overlay_root, std::string_view guest_path) {
|
||||
std::filesystem::path path = overlay_root;
|
||||
size_t segment_start = 0;
|
||||
while (segment_start < guest_path.size()) {
|
||||
const size_t slash = guest_path.find('/', segment_start);
|
||||
const size_t segment_end = slash == std::string_view::npos ? guest_path.size() : slash;
|
||||
if (segment_end > segment_start) {
|
||||
path /= std::string(guest_path.substr(segment_start, segment_end - segment_start));
|
||||
}
|
||||
if (slash == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
segment_start = slash + 1;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path, ec);
|
||||
return !ec;
|
||||
}
|
||||
|
||||
void Skate3BaseApp::InstallRecipeOverlay() {
|
||||
if (recipe_overlay_installed_ || !runtime() || !runtime()->file_system()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto content_root = game_data_root() / "data" / "content";
|
||||
const auto content_root = runtime()->game_data_root() / "data" / "content";
|
||||
if (!std::filesystem::exists(content_root)) {
|
||||
REXLOG_WARN("Skipping Skate 3 recipe VFS overlay; content root not found: {}",
|
||||
content_root.string());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto aliases = DiscoverRecipeAliases(content_root);
|
||||
if (aliases.empty()) {
|
||||
REXLOG_WARN("Skipping Skate 3 recipe VFS overlay; no recipe aliases found in {}",
|
||||
content_root.string());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto overlay_root = cache_root() / "vfs_recipe_aliases";
|
||||
const auto overlay_root = cache_root() / "vfs_big_directory_aliases";
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(overlay_root, ec);
|
||||
if (ec) {
|
||||
REXLOG_WARN("Skipping Skate 3 BIG-directory VFS overlay; failed to create {}: {}",
|
||||
overlay_root.string(), ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
size_t created = 0;
|
||||
for (const auto& alias : aliases) {
|
||||
std::filesystem::create_directories(overlay_root / alias, ec);
|
||||
if (!ec) {
|
||||
const std::string guest_path = std::string("data/content/recipe/") + alias;
|
||||
if (CreateOverlayDirectory(overlay_root, guest_path)) {
|
||||
++created;
|
||||
}
|
||||
ec.clear();
|
||||
}
|
||||
|
||||
static constexpr std::string_view kBigDirectoryAliases[] = {
|
||||
"data/scene",
|
||||
"data/scene/anim",
|
||||
"data/scene/trickguide",
|
||||
"data/livingworld/PluginDescriptor",
|
||||
"data/state/livingworldentities/pedestrian/plugin",
|
||||
};
|
||||
for (std::string_view alias : kBigDirectoryAliases) {
|
||||
if (CreateOverlayDirectory(overlay_root, alias)) {
|
||||
++created;
|
||||
}
|
||||
}
|
||||
|
||||
if (!created) {
|
||||
REXLOG_WARN("Skipping Skate 3 BIG-directory VFS overlay; failed to create alias directories in {}",
|
||||
overlay_root.string());
|
||||
return;
|
||||
}
|
||||
|
||||
auto device = std::make_unique<rex::filesystem::HostPathDevice>(
|
||||
"skate3recipe:", overlay_root, true);
|
||||
"skate3bigdirs:", overlay_root, true);
|
||||
if (!device->Initialize()) {
|
||||
REXLOG_WARN("Skipping Skate 3 BIG-directory VFS overlay; failed to initialize host device for {}",
|
||||
overlay_root.string());
|
||||
return;
|
||||
}
|
||||
|
||||
runtime()->file_system()->RegisterDevice(std::move(device));
|
||||
runtime()->file_system()->RegisterSymbolicLink(
|
||||
"\\Device\\Harddisk0\\Partition1\\data\\content\\recipe",
|
||||
"skate3recipe:");
|
||||
"skate3bigdirs:\\data\\content\\recipe");
|
||||
runtime()->file_system()->RegisterSymbolicLink(
|
||||
"\\Device\\Harddisk0\\Partition1\\data\\scene",
|
||||
"skate3bigdirs:\\data\\scene");
|
||||
runtime()->file_system()->RegisterSymbolicLink(
|
||||
"\\Device\\Harddisk0\\Partition1\\data\\livingworld\\PluginDescriptor",
|
||||
"skate3bigdirs:\\data\\livingworld\\PluginDescriptor");
|
||||
runtime()->file_system()->RegisterSymbolicLink(
|
||||
"\\Device\\Harddisk0\\Partition1\\data\\state\\livingworldentities\\pedestrian\\plugin",
|
||||
"skate3bigdirs:\\data\\state\\livingworldentities\\pedestrian\\plugin");
|
||||
recipe_overlay_installed_ = true;
|
||||
REXLOG_INFO("Installed Skate 3 BIG-directory VFS overlay with {} aliases from {}",
|
||||
created, content_root.string());
|
||||
}
|
||||
|
||||
void Skate3BaseApp::InstallBigDeviceAliases() {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <rex/rex_app.h>
|
||||
#include <rex/ui/overlay/simple_settings_overlay.h>
|
||||
|
|
@ -31,12 +30,9 @@ class Skate3BaseApp : public rex::ReXApp {
|
|||
void OnConfigureFonts(ImFontAtlas* atlas) override;
|
||||
void OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) override;
|
||||
void OnPostSetup() override;
|
||||
void OnPostLaunchModule(rex::system::XThread* thread) override;
|
||||
void OnShutdown() override;
|
||||
void OnGuestThreadExit(rex::system::XThread* thread) override;
|
||||
|
||||
private:
|
||||
void StopGpuMemoryInvalidationThread();
|
||||
void InstallRecipeOverlay();
|
||||
void InstallBigDeviceAliases();
|
||||
void ToggleSimpleSettings();
|
||||
|
|
@ -44,11 +40,15 @@ class Skate3BaseApp : public rex::ReXApp {
|
|||
void ApplyGameplayCursorMode();
|
||||
void RestartGame();
|
||||
void SaveDrawFingerprintLog();
|
||||
void LogUserMarker();
|
||||
void LogDebugMarker();
|
||||
void ApplySelectedProfileToRuntime();
|
||||
|
||||
static bool IsRecipeNameChar(char c);
|
||||
static std::set<std::string> DiscoverRecipeAliases(
|
||||
const std::filesystem::path& content_root);
|
||||
static bool CreateOverlayDirectory(const std::filesystem::path& overlay_root,
|
||||
std::string_view guest_path);
|
||||
|
||||
std::filesystem::path config_path_;
|
||||
std::filesystem::path user_settings_path_;
|
||||
|
|
@ -56,6 +56,5 @@ class Skate3BaseApp : public rex::ReXApp {
|
|||
std::unique_ptr<rex::ui::SimpleSettingsDialog> simple_settings_dialog_;
|
||||
bool recipe_overlay_installed_ = false;
|
||||
bool big_device_aliases_installed_ = false;
|
||||
std::atomic<bool> invalidate_gpu_memory_thread_running_{false};
|
||||
std::thread invalidate_gpu_memory_thread_;
|
||||
std::atomic<uint32_t> debug_marker_count_{0};
|
||||
};
|
||||
|
|
|
|||
2
third_party/rexglue-sdk
vendored
2
third_party/rexglue-sdk
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 3184592404a70a4c1fcd07c8dd6f2494c4bd8b4f
|
||||
Subproject commit 5b6b87bcea1d70b6922815890bb81adc929ea106
|
||||
Loading…
Add table
Reference in a new issue