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}

", + "
") + 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