From 0662d0a92d8fdb69525c2ddc34ac1ef3eb37bd6c Mon Sep 17 00:00:00 2001
From: mchughalex <25671999+mchughalex@users.noreply.github.com>
Date: Sat, 6 Jun 2026 15:15:39 +0100
Subject: [PATCH] add Linux diagnostics and texture preview tools
---
config/skate3.example.toml | 1 +
scripts/decode_xenos_texture_dumps.py | 212 +++++++++++++++++
scripts/render_static_rx2_textures.py | 317 ++++++++++++++++++++++++++
src/skate3_app_common.cpp | 153 +++++++++----
src/skate3_app_common.h | 11 +-
third_party/rexglue-sdk | 2 +-
6 files changed, 640 insertions(+), 56 deletions(-)
create mode 100755 scripts/decode_xenos_texture_dumps.py
create mode 100755 scripts/render_static_rx2_textures.py
diff --git a/config/skate3.example.toml b/config/skate3.example.toml
index 70b973a..78c4399 100644
--- a/config/skate3.example.toml
+++ b/config/skate3.example.toml
@@ -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
diff --git a/scripts/decode_xenos_texture_dumps.py b/scripts/decode_xenos_texture_dumps.py
new file mode 100755
index 0000000..5411492
--- /dev/null
+++ b/scripts/decode_xenos_texture_dumps.py
@@ -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 = [
+ "
Texture dumps",
+ "",
+ "Texture dumps
",
+ ]
+ for raw_path, address, length, width, height, images in rows:
+ parts.append(
+ f"{html.escape(raw_path.name)}
"
+ f"address 0x{address:08X}, length 0x{length:X}, "
+ f"{width}x{height}
"
+ )
+ for image_path in images:
+ label = image_path.name.removeprefix(raw_path.stem + "_").removesuffix(".png")
+ parts.append(
+ f"
{html.escape(label)}
"
+ f"
})
"
+ )
+ parts.append("
")
+ 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())
diff --git a/scripts/render_static_rx2_textures.py b/scripts/render_static_rx2_textures.py
new file mode 100755
index 0000000..558e305
--- /dev/null
+++ b/scripts/render_static_rx2_textures.py
@@ -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(" 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 = [
+ "Skate 3 RX2 textures",
+ "",
+ f"Skate 3 RX2 Textures: Page {page_number} / {page_count}
",
+ "",
+ "
Index",
+ ]
+ if page_number > 1:
+ rows.append(f"
Previous")
+ if page_number < page_count:
+ rows.append(f"
Next")
+ rows.append("
")
+ for source, image in rendered:
+ rel_image = image.relative_to(output_dir)
+ rows.append(
+ f"
)})
"
+ f"
{html.escape(source.name)}
"
+ )
+ rows.append("
")
+ (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 = [
+ "Skate 3 RX2 textures",
+ "",
+ "Skate 3 RX2 Textures
",
+ f"{len(rendered)} textures rendered. Open one page at a time to avoid loading every PNG at once.
",
+ "",
+ ]
+ 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"
Page {index}
{start}-{end}"
+ )
+ rows.append("
")
+ (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())
diff --git a/src/skate3_app_common.cpp b/src/skate3_app_common.cpp
index cb50b4f..d0f50d3 100644
--- a/src/skate3_app_common.cpp
+++ b/src/skate3_app_common.cpp
@@ -37,7 +37,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -185,19 +184,24 @@ bool ConfigContainsAnyKey(const toml::table& table, std::initializer_list 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(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 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(
- "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() {
diff --git a/src/skate3_app_common.h b/src/skate3_app_common.h
index a5da3c7..e0f95ee 100644
--- a/src/skate3_app_common.h
+++ b/src/skate3_app_common.h
@@ -9,7 +9,6 @@
#include
#include
#include
-#include
#include
#include
@@ -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 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 simple_settings_dialog_;
bool recipe_overlay_installed_ = false;
bool big_device_aliases_installed_ = false;
- std::atomic invalidate_gpu_memory_thread_running_{false};
- std::thread invalidate_gpu_memory_thread_;
+ std::atomic debug_marker_count_{0};
};
diff --git a/third_party/rexglue-sdk b/third_party/rexglue-sdk
index 3184592..5b6b87b 160000
--- a/third_party/rexglue-sdk
+++ b/third_party/rexglue-sdk
@@ -1 +1 @@
-Subproject commit 3184592404a70a4c1fcd07c8dd6f2494c4bd8b4f
+Subproject commit 5b6b87bcea1d70b6922815890bb81adc929ea106