add Linux diagnostics and texture preview tools

This commit is contained in:
mchughalex 2026-06-06 15:15:39 +01:00
parent 943d26e2d9
commit 0662d0a92d
6 changed files with 640 additions and 56 deletions

View file

@ -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

View 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())

View 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())

View file

@ -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() {

View file

@ -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};
};

@ -1 +1 @@
Subproject commit 3184592404a70a4c1fcd07c8dd6f2494c4bd8b4f
Subproject commit 5b6b87bcea1d70b6922815890bb81adc929ea106