add ultrawide and live FOV support

This commit is contained in:
mchughalex 2026-06-09 21:34:17 +01:00
parent ec224f7d66
commit 1ed2841c5c
15 changed files with 8042 additions and 28 deletions

5
.gitignore vendored
View file

@ -2,6 +2,8 @@
/out/ /out/
/build/ /build/
/generated/ /generated/
/generated.backup-*/
/tools/
# Local game dump and retail media. Users must provide their own copy. # Local game dump and retail media. Users must provide their own copy.
/game/ /game/
@ -29,3 +31,6 @@ CMakeUserPresets.json
/.vs/ /.vs/
/.vscode/ /.vscode/
/.cache/ /.cache/
/scripts/__pycache__/
/scripts/extract_eb_big.py
/scripts/extract_embedded_rx2.py

View file

@ -107,6 +107,7 @@ include(cmake/CodegenTargets.cmake)
set(SKATE3_COMMON_SOURCES set(SKATE3_COMMON_SOURCES
src/exception_compat.cpp src/exception_compat.cpp
src/skate3_app_common.cpp src/skate3_app_common.cpp
src/skate3_fov.cpp
src/skate3_iso_installer.cpp src/skate3_iso_installer.cpp
src/skate3_user_settings.cpp src/skate3_user_settings.cpp
) )

View file

@ -48,16 +48,13 @@ Gameplay (click to go to YouTube):
6. Wait for the installer to extract the game files. 6. Wait for the installer to extract the game files.
7. Click "Start Game". 7. Click "Start Game".
### DLC ## Installing DLC
Skate3Recomp does not include Skate 3 DLC files. To use DLC, provide package To use DLC, you must provide package files from your own legally obtained Xbox 360 DLC.
files from your own legally obtained Xbox 360 DLC.
Create a `dlc` folder beside the executable, inside the installed game folder, Create a `dlc` folder either beside the executable, inside the installed game folder,
or in the Skate3Recomp user data folder. Place the DLC package files in that or in the user data folder. Place the DLC package files in that
folder and start the game. Skate3Recomp will validate matching Skate 3 folder and start the game.
marketplace packages and install them into the local content store
automatically.
## Controls ## Controls
@ -201,6 +198,10 @@ SKATE3_MNK=1 ./scripts/run-linux.sh
``` ```
Fullscreen is on by default. Pass `--no-fullscreen` to start windowed. Fullscreen is on by default. Pass `--no-fullscreen` to start windowed.
On Windows, ultrawide displays are detected automatically in fullscreen. For an
ultrawide window, pass matching `--window_width` and `--window_height` values;
the renderer keeps the guest frontbuffer at a stable 16:9 size while applying
ultrawide presentation and Hor+ correction without stretching.
To run the Linux development executable directly: To run the Linux development executable directly:

View file

@ -0,0 +1,86 @@
if(NOT DEFINED SKATE3_SOURCE_DIR)
message(FATAL_ERROR "SKATE3_SOURCE_DIR is required")
endif()
set(_skate3_recomp_39 "${SKATE3_SOURCE_DIR}/generated/skate3_recomp.39.cpp")
if(NOT EXISTS "${_skate3_recomp_39}")
message(FATAL_ERROR "Expected generated file not found: ${_skate3_recomp_39}")
endif()
file(READ "${_skate3_recomp_39}" _contents)
if(NOT _contents MATCHES "#include \"skate3_ultrawide_guest\\.h\"")
string(REPLACE
"#include \"skate3_init.h\"\n"
"#include \"skate3_init.h\"\n#include \"skate3_ultrawide_guest.h\"\n"
_contents
"${_contents}")
endif()
set(_frustum_call
" // bl 0x827f2330
ctx.lr = 0x827F2AE8;
sub_827F2330(ctx, base);")
set(_frustum_patch
" // bl 0x827f2330
ctx.lr = 0x827F2AE8;
Skate3UltrawideGameFrustumPatchScope skate3_ultrawide_game_frustum_patch_scope(
ctx, base, ctx.r4.u32);
sub_827F2330(ctx, base);")
if(_contents MATCHES "Skate3UltrawideGameFrustumPatchScope")
message(STATUS "Skate 3 generated frustum patch already applied")
elseif(_contents MATCHES "sub_827F2330\\(ctx, base\\);")
string(REPLACE "${_frustum_call}" "${_frustum_patch}" _patched "${_contents}")
if(_patched STREQUAL _contents)
message(FATAL_ERROR "Failed to apply Skate 3 generated frustum patch; expected call-site context changed")
endif()
set(_contents "${_patched}")
else()
message(FATAL_ERROR "Failed to apply Skate 3 generated frustum patch; call to sub_827F2330 not found")
endif()
file(WRITE "${_skate3_recomp_39}" "${_contents}")
message(STATUS "Applied Skate 3 generated frustum patch")
set(_skate3_recomp_38 "${SKATE3_SOURCE_DIR}/generated/skate3_recomp.38.cpp")
if(NOT EXISTS "${_skate3_recomp_38}")
message(FATAL_ERROR "Expected generated file not found: ${_skate3_recomp_38}")
endif()
file(READ "${_skate3_recomp_38}" _contents)
if(NOT _contents MATCHES "#include \"skate3_fov\\.h\"")
string(REPLACE
"#include \"skate3_init.h\"\n"
"#include \"skate3_init.h\"\n#include \"skate3_fov.h\"\n"
_contents
"${_contents}")
endif()
set(_projection_fov_site
"DEFINE_REX_FUNC(sub_827DE960) {
REX_FUNC_PROLOGUE();
PPCRegister temp{};
uint32_t ea{};")
set(_projection_fov_patch
"DEFINE_REX_FUNC(sub_827DE960) {
REX_FUNC_PROLOGUE();
PPCRegister temp{};
uint32_t ea{};
ctx.f1.f64 = double(Skate3MaybeOverrideProjectionFovRadians(float(ctx.f1.f64)));")
if(_contents MATCHES "Skate3MaybeOverrideProjectionFovRadians")
message(STATUS "Skate 3 generated projection FOV patch already applied")
else()
string(REPLACE "${_projection_fov_site}" "${_projection_fov_patch}" _patched "${_contents}")
if(_patched STREQUAL _contents)
message(FATAL_ERROR "Failed to apply Skate 3 generated projection FOV patch; expected function prologue changed")
endif()
set(_contents "${_patched}")
endif()
file(WRITE "${_skate3_recomp_38}" "${_contents}")
message(STATUS "Applied Skate 3 generated projection FOV patch")

View file

@ -1,6 +1,9 @@
add_custom_target(generate-skate3 add_custom_target(generate-skate3
COMMAND $<TARGET_FILE:rex::rexglue> codegen COMMAND $<TARGET_FILE:rex::rexglue> codegen
"${CMAKE_CURRENT_BINARY_DIR}/manifests/skate3.toml" "${CMAKE_CURRENT_BINARY_DIR}/manifests/skate3.toml"
COMMAND "${CMAKE_COMMAND}"
"-DSKATE3_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}"
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ApplySkate3CodegenPatches.cmake"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Generating recompiled code for default.xex" COMMENT "Generating recompiled code for default.xex"
VERBATIM VERBATIM

View file

@ -16,6 +16,20 @@ mnk_mode = false
mnk_capture_mouse = false mnk_capture_mouse = false
fullscreen = true fullscreen = true
# Automatically derive an ultrawide host aspect from the fullscreen monitor on
# Windows, or from an explicit window_width/window_height pair. Leave
# video_mode_width and video_mode_height at defaults for this path.
skate3_ultrawide = false
skate3_field_of_view = 60.0
skate3_ultrawide_base_height = 720
skate3_ultrawide_hor_plus = true
skate3_ultrawide_hor_plus_scale = 0.0
# Risky diagnostics for narrowing texture/visibility pop-in. Enable one at a
# time when testing.
skate3_ultrawide_force_main_visibility_flags = true
skate3_ultrawide_ignore_streamer_texture_invalidations = false
# window_width = 3440
# window_height = 1440
vsync = false vsync = false
video_mode_refresh_rate = 60.0 video_mode_refresh_rate = 60.0
resolution_scale = 2 resolution_scale = 2

View file

@ -158,8 +158,15 @@ def render_rx2(
return output_path return output_path
def display_source_name(source: Path, root: Path | None) -> str:
try:
return str(source.relative_to(root)) if root else str(source)
except ValueError:
return str(source)
def write_texture_page( def write_texture_page(
rendered: list[tuple[Path, Path]], rendered: list[tuple[Path, Path, str]],
output_dir: Path, output_dir: Path,
page_number: int, page_number: int,
page_count: int, page_count: int,
@ -182,18 +189,18 @@ def write_texture_page(
if page_number < page_count: if page_number < page_count:
rows.append(f"<a href='page-{page_number + 1:03d}.html'>Next</a>") rows.append(f"<a href='page-{page_number + 1:03d}.html'>Next</a>")
rows.append("</div><div class='grid'>") rows.append("</div><div class='grid'>")
for source, image in rendered: for source, image, label in rendered:
rel_image = image.relative_to(output_dir) rel_image = image.relative_to(output_dir)
rows.append( rows.append(
f"<div class='item'><img loading='lazy' src='{html.escape(str(rel_image))}'>" f"<div class='item'><img loading='lazy' src='{html.escape(str(rel_image))}'>"
f"<p><code>{html.escape(source.name)}</code></p></div>" f"<p><code>{html.escape(label)}</code></p></div>"
) )
rows.append("</div>") rows.append("</div>")
(output_dir / page_name).write_text("\n".join(rows), encoding="utf-8") (output_dir / page_name).write_text("\n".join(rows), encoding="utf-8")
return page_name return page_name
def write_index(rendered: list[tuple[Path, Path]], output_dir: Path, page_size: int) -> None: def write_index(rendered: list[tuple[Path, Path, str]], output_dir: Path, page_size: int) -> None:
page_size = max(1, page_size) page_size = max(1, page_size)
pages = [rendered[offset : offset + page_size] for offset in range(0, len(rendered), page_size)] pages = [rendered[offset : offset + page_size] for offset in range(0, len(rendered), page_size)]
page_names = [ page_names = [
@ -297,11 +304,11 @@ def main() -> int:
output_dir = Path(args.output_dir) output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
rendered: list[tuple[Path, Path]] = [] rendered: list[tuple[Path, Path, str]] = []
for source in sources: for source in sources:
image = render_rx2(source, output_dir, source_root, args.layout) image = render_rx2(source, output_dir, source_root, args.layout)
if image: if image:
rendered.append((source, image)) rendered.append((source, image, display_source_name(source, source_root)))
if args.verbose: if args.verbose:
print(f"Rendered {source.name}") print(f"Rendered {source.name}")
if args.limit and len(rendered) >= args.limit: if args.limit and len(rendered) >= args.limit:

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
#include "skate3_app_common.h" #include "skate3_app_common.h"
#include "skate3_fov.h"
#include "skate3_iso_installer.h" #include "skate3_iso_installer.h"
#include "skate3_user_settings.h" #include "skate3_user_settings.h"
@ -12,6 +13,7 @@
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <memory> #include <memory>
#include <optional>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <string_view> #include <string_view>
@ -39,6 +41,8 @@
#include <rex/filesystem/devices/stfs_container_device.h> #include <rex/filesystem/devices/stfs_container_device.h>
#include <rex/filesystem/devices/host_path_device.h> #include <rex/filesystem/devices/host_path_device.h>
#include <rex/filesystem/vfs.h> #include <rex/filesystem/vfs.h>
#include <rex/graphics/flags.h>
#include <rex/graphics/ultrawide_debug.h>
#include <rex/input/input_system.h> #include <rex/input/input_system.h>
#include <rex/kernel/xam/module.h> #include <rex/kernel/xam/module.h>
#include <rex/logging.h> #include <rex/logging.h>
@ -51,8 +55,10 @@
#include <rex/system/xam/content_manager.h> #include <rex/system/xam/content_manager.h>
#include <rex/system/xam/user_profile.h> #include <rex/system/xam/user_profile.h>
#include <rex/system.h> #include <rex/system.h>
#include <rex/ui/flags.h>
#include <rex/ui/keybinds.h> #include <rex/ui/keybinds.h>
#include <rex/ui/overlay/simple_settings_overlay.h> #include <rex/ui/overlay/simple_settings_overlay.h>
#include <rex/ui/overlay/ultrawide_targets_overlay.h>
#include <imgui.h> #include <imgui.h>
#include <toml++/toml.hpp> #include <toml++/toml.hpp>
@ -71,6 +77,46 @@ REXCVAR_DEFINE_STRING(skate3_dlc_root, "", "Skate 3",
"Directory containing Skate 3 DLC package files"); "Directory containing Skate 3 DLC package files");
REXCVAR_DEFINE_BOOL(skate3_auto_install_dlc, true, "Skate 3", REXCVAR_DEFINE_BOOL(skate3_auto_install_dlc, true, "Skate 3",
"Install DLC package files found in configured DLC folders"); "Install DLC package files found in configured DLC folders");
REXCVAR_DEFINE_BOOL(skate3_ultrawide, false, "Skate 3",
"Automatically derive an ultrawide guest video mode from the host display");
REXCVAR_DEFINE_DOUBLE(skate3_field_of_view, 60.0, "Skate 3",
"Gameplay camera field of view in degrees")
.range(40.0, 120.0);
REXCVAR_DEFINE_INT32(skate3_ultrawide_base_height, 720, "Skate 3",
"Guest video mode height used when deriving ultrawide modes")
.range(480, 2160)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_BOOL(skate3_ultrawide_hor_plus, true, "Skate 3",
"Apply Hor+ clip-space correction for ultrawide video modes");
REXCVAR_DEFINE_DOUBLE(skate3_ultrawide_hor_plus_scale, 0.0, "Skate 3",
"Manual Hor+ X scale override (0 = derive from host aspect)")
.range(0.0, 4.0)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_BOOL(skate3_ultrawide_disable_ndc_correction, false, "Skate 3",
"Diagnostic: disable the GPU NDC Hor+ correction while preserving guest-side "
"ultrawide diagnostics");
REXCVAR_DEFINE_BOOL(skate3_ultrawide_trace_draws, false, "Skate 3",
"Diagnostic: collect live draw fingerprints for the F7 ultrawide overlay");
REXCVAR_DEFINE_BOOL(skate3_ultrawide_object_stage_trace_to_disk, false, "Skate 3",
"Diagnostic: write object render-stage transition summaries to disk");
REXCVAR_DEFINE_BOOL(skate3_ultrawide_object_stage_trace_continuous, false, "Skate 3",
"Diagnostic: continuously trace object render-stage summaries");
REXCVAR_DEFINE_INT32(skate3_ultrawide_object_stage_trace_frames_remaining, 0, "Skate 3",
"Diagnostic: trace this many object render-stage frames")
.range(0, 1000000);
REXCVAR_DEFINE_INT32(skate3_ultrawide_object_stage_marker, 0, "Skate 3",
"Diagnostic: mark the next object-stage frame (1 visible, 2 invisible)")
.range(0, 2);
REXCVAR_DEFINE_INT32(skate3_ultrawide_object_stage_missing_limit, 256, "Skate 3",
"Diagnostic: maximum missing-main objects to log per object-stage frame")
.range(0, 4096);
REXCVAR_DEFINE_BOOL(skate3_ultrawide_force_main_visibility_flags, false, "Skate 3",
"Force main render object visibility flags while Hor+ ultrawide is active");
REXCVAR_DEFINE_BOOL(skate3_ultrawide_widen_game_frustum, true, "Skate 3",
"Widen the game-side main-world cull frustum to match the ultrawide view");
REXCVAR_DEFINE_DOUBLE(skate3_ultrawide_target_aspect, 0.0, "Skate 3",
"Derived host display aspect for ultrawide presentation (0 = disabled)")
.range(0.0, 8.0);
namespace { namespace {
@ -137,6 +183,141 @@ void SetRestartArgument(std::vector<std::string>& args, std::string name, std::s
constexpr std::string_view kUserDirectoryName = "skate3"; constexpr std::string_view kUserDirectoryName = "skate3";
constexpr std::string_view kSettingsFilename = "settings.toml"; constexpr std::string_view kSettingsFilename = "settings.toml";
constexpr std::string_view kDlcDirectoryName = "dlc"; constexpr std::string_view kDlcDirectoryName = "dlc";
constexpr double kSixteenNineAspect = 16.0 / 9.0;
constexpr double kUltrawideAspectEpsilon = 0.01;
struct DisplaySize {
int32_t width = 0;
int32_t height = 0;
};
#if defined(_WIN32)
BOOL CALLBACK CollectMonitorCallback(HMONITOR monitor_handle, HDC, LPRECT, LPARAM data) {
auto* monitors = reinterpret_cast<std::vector<HMONITOR>*>(data);
monitors->push_back(monitor_handle);
return TRUE;
}
HMONITOR GetConfiguredMonitorHandle() {
const int32_t monitor_index = REXCVAR_GET(monitor);
if (monitor_index > 0) {
std::vector<HMONITOR> monitors;
EnumDisplayMonitors(nullptr, nullptr, CollectMonitorCallback,
reinterpret_cast<LPARAM>(&monitors));
if (monitor_index <= static_cast<int32_t>(monitors.size())) {
return monitors[monitor_index - 1];
}
}
POINT origin{0, 0};
return MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY);
}
std::optional<DisplaySize> QueryFullscreenMonitorSize() {
HMONITOR monitor_handle = GetConfiguredMonitorHandle();
if (!monitor_handle) {
return std::nullopt;
}
MONITORINFO monitor_info{};
monitor_info.cbSize = sizeof(monitor_info);
if (!GetMonitorInfo(monitor_handle, &monitor_info)) {
return std::nullopt;
}
const int32_t width = monitor_info.rcMonitor.right - monitor_info.rcMonitor.left;
const int32_t height = monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top;
if (width <= 0 || height <= 0) {
return std::nullopt;
}
return DisplaySize{width, height};
}
#else
std::optional<DisplaySize> QueryFullscreenMonitorSize() {
return std::nullopt;
}
#endif
std::optional<DisplaySize> ResolveUltrawideTargetDisplaySize() {
if (rex::cvar::HasNonDefaultValue("resolution")) {
return std::nullopt;
}
if (REXCVAR_GET(fullscreen)) {
return QueryFullscreenMonitorSize();
}
const int32_t configured_window_width = REXCVAR_GET(window_width);
const int32_t configured_window_height = REXCVAR_GET(window_height);
if (rex::cvar::HasNonDefaultValue("window_width") &&
rex::cvar::HasNonDefaultValue("window_height") && configured_window_width > 0 &&
configured_window_height > 0) {
return DisplaySize{configured_window_width, configured_window_height};
}
return std::nullopt;
}
void ApplyUltrawideVideoDefaults() {
if (!REXCVAR_GET(skate3_ultrawide) ||
rex::cvar::HasNonDefaultValue("skate3_ultrawide_target_aspect")) {
return;
}
const std::optional<DisplaySize> target_size = ResolveUltrawideTargetDisplaySize();
if (!target_size || target_size->width <= 0 || target_size->height <= 0) {
return;
}
const double target_aspect =
static_cast<double>(target_size->width) / static_cast<double>(target_size->height);
if (target_aspect <= kSixteenNineAspect + kUltrawideAspectEpsilon) {
return;
}
rex::cvar::SetFlagByName("skate3_ultrawide_target_aspect", std::to_string(target_aspect));
if (!rex::cvar::HasNonDefaultValue("present_letterbox")) {
rex::cvar::SetFlagByName("present_letterbox", "true");
}
}
void DisableActiveUltrawideDiagnostics() {
constexpr std::string_view kFalseFlags[] = {
"skate3_ultrawide_trace_draws",
"skate3_ultrawide_object_stage_trace_to_disk",
"skate3_ultrawide_object_stage_trace_continuous",
"skate3_ultrawide_force_main_visibility_flags",
"skate3_ultrawide_texture_trace_bind_keys",
"skate3_ultrawide_texture_trace_scaled_resolve",
"skate3_ultrawide_ignore_streamer_texture_invalidations",
"skate3_ultrawide_screen_callback_tracking",
"skate3_ultrawide_fake_occlusion_queries",
"perf_draw_fingerprints",
"perf_keep_heavyweight_draw_diagnostics",
"trace_gpu_stream",
};
for (std::string_view flag : kFalseFlags) {
rex::cvar::SetFlagByName(std::string(flag), "false");
}
constexpr std::string_view kZeroFlags[] = {
"skate3_ultrawide_texture_trace_remaining",
"skate3_ultrawide_object_stage_trace_frames_remaining",
"skate3_ultrawide_object_stage_marker",
"vulkan_debug_log_frame_summaries_remaining",
"vulkan_debug_log_resolve_decisions_remaining",
"vulkan_debug_log_team_profile_background_candidates_remaining",
"vulkan_debug_log_team_profile_background_bindings_remaining",
"filesystem_debug_log_fe_asset_ops_remaining",
"filesystem_debug_log_team_profile_background_remaining",
};
for (std::string_view flag : kZeroFlags) {
rex::cvar::SetFlagByName(std::string(flag), "0");
}
rex::cvar::SetFlagByName("skate3_ultrawide_fake_occlusion_sample_count", "1000");
}
std::filesystem::path DefaultDocumentsUserRoot() { std::filesystem::path DefaultDocumentsUserRoot() {
return rex::filesystem::GetUserFolder() / std::string(kUserDirectoryName); return rex::filesystem::GetUserFolder() / std::string(kUserDirectoryName);
@ -342,6 +523,13 @@ void Skate3BaseApp::OnConfigurePaths(rex::PathConfig& paths) {
ConfigureSkate3UserPaths(paths, user_settings_path_, profiles_path_); ConfigureSkate3UserPaths(paths, user_settings_path_, profiles_path_);
config_path_ = paths.config_path; config_path_ = paths.config_path;
LoadAndNormalizeSimpleSettings(user_settings_path_, config_path_); LoadAndNormalizeSimpleSettings(user_settings_path_, config_path_);
Skate3InitializeFieldOfViewOverride();
ApplyUltrawideVideoDefaults();
if (!rex::graphics::ultrawide_debug::LoadTargets(paths.cache_root /
"skate3_ultrawide_targets.toml")) {
rex::graphics::ultrawide_debug::LoadBuiltInSkate3Classifier();
}
DisableActiveUltrawideDiagnostics();
} }
void Skate3BaseApp::OnConfigureFonts(ImFontAtlas* atlas) { void Skate3BaseApp::OnConfigureFonts(ImFontAtlas* atlas) {
@ -449,6 +637,10 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) {
rex::ui::RegisterBind("bind_skate3_menu_alt", "F1", "Skate 3 settings alternate", [this] { rex::ui::RegisterBind("bind_skate3_menu_alt", "F1", "Skate 3 settings alternate", [this] {
ToggleSimpleSettings(); ToggleSimpleSettings();
}); });
rex::ui::RegisterBind("bind_skate3_ultrawide_targets", "F7",
"Skate 3 ultrawide targets", [this] {
ToggleUltrawideTargets();
});
rex::ui::RegisterBind("bind_skate3_save_draw_fingerprints", "F8", rex::ui::RegisterBind("bind_skate3_save_draw_fingerprints", "F8",
"Save draw fingerprint log", [this] { "Save draw fingerprint log", [this] {
SaveDrawFingerprintLog(); SaveDrawFingerprintLog();
@ -506,11 +698,13 @@ void Skate3BaseApp::OnPostSetup() {
void Skate3BaseApp::OnShutdown() { void Skate3BaseApp::OnShutdown() {
rex::ui::UnregisterBind("bind_skate3_menu"); rex::ui::UnregisterBind("bind_skate3_menu");
rex::ui::UnregisterBind("bind_skate3_menu_alt"); rex::ui::UnregisterBind("bind_skate3_menu_alt");
rex::ui::UnregisterBind("bind_skate3_ultrawide_targets");
rex::ui::UnregisterBind("bind_skate3_save_draw_fingerprints"); rex::ui::UnregisterBind("bind_skate3_save_draw_fingerprints");
rex::ui::UnregisterBind("bind_skate3_log_debug_marker"); rex::ui::UnregisterBind("bind_skate3_log_debug_marker");
rex::ui::UnregisterBind("bind_skate3_log_user_marker"); rex::ui::UnregisterBind("bind_skate3_log_user_marker");
ApplyGameplayCursorMode(); ApplyGameplayCursorMode();
simple_settings_dialog_.reset(); simple_settings_dialog_.reset();
ultrawide_targets_dialog_.reset();
} }
void Skate3BaseApp::ToggleSimpleSettings() { void Skate3BaseApp::ToggleSimpleSettings() {
@ -588,6 +782,24 @@ void Skate3BaseApp::ToggleSimpleSettings() {
simple_settings_dialog_->Show(); simple_settings_dialog_->Show();
} }
void Skate3BaseApp::ToggleUltrawideTargets() {
if (ultrawide_targets_dialog_) {
if (ultrawide_targets_dialog_->visible()) {
ultrawide_targets_dialog_->Hide();
} else {
ApplySettingsCursorMode();
ultrawide_targets_dialog_->Show();
}
return;
}
const auto export_path = cache_root() / "skate3_ultrawide_targets.toml";
ultrawide_targets_dialog_ = std::make_unique<rex::ui::UltrawideTargetsDialog>(
imgui_drawer(), export_path, [this]() { ApplyGameplayCursorMode(); });
ApplySettingsCursorMode();
ultrawide_targets_dialog_->Show();
}
void Skate3BaseApp::ApplySettingsCursorMode() { void Skate3BaseApp::ApplySettingsCursorMode() {
if (window()) { if (window()) {
window()->SetCursorVisibility(rex::ui::Window::CursorVisibility::kVisible); window()->SetCursorVisibility(rex::ui::Window::CursorVisibility::kVisible);
@ -714,19 +926,8 @@ void Skate3BaseApp::LogUserMarker() {
void Skate3BaseApp::LogDebugMarker() { void Skate3BaseApp::LogDebugMarker() {
const uint32_t marker = debug_marker_count_.fetch_add(1, std::memory_order_relaxed) + 1; 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"); DisableActiveUltrawideDiagnostics();
rex::cvar::SetFlagByName("vulkan_debug_frame_summary_interval_frames", "300"); REXLOG_WARN("USER DEBUG MARKER #{}: F9 pressed; active diagnostics disabled", marker);
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() { void Skate3BaseApp::ApplySelectedProfileToRuntime() {

View file

@ -12,6 +12,7 @@
#include <rex/rex_app.h> #include <rex/rex_app.h>
#include <rex/ui/overlay/simple_settings_overlay.h> #include <rex/ui/overlay/simple_settings_overlay.h>
#include <rex/ui/overlay/ultrawide_targets_overlay.h>
namespace rex::ui { namespace rex::ui {
class ImGuiDrawer; class ImGuiDrawer;
@ -37,6 +38,7 @@ class Skate3BaseApp : public rex::ReXApp {
void InstallBigDeviceAliases(); void InstallBigDeviceAliases();
void InstallDlcPackages(); void InstallDlcPackages();
void ToggleSimpleSettings(); void ToggleSimpleSettings();
void ToggleUltrawideTargets();
void ApplySettingsCursorMode(); void ApplySettingsCursorMode();
void ApplyGameplayCursorMode(); void ApplyGameplayCursorMode();
void RestartGame(); void RestartGame();
@ -55,6 +57,7 @@ class Skate3BaseApp : public rex::ReXApp {
std::filesystem::path user_settings_path_; std::filesystem::path user_settings_path_;
std::filesystem::path profiles_path_; std::filesystem::path profiles_path_;
std::unique_ptr<rex::ui::SimpleSettingsDialog> simple_settings_dialog_; std::unique_ptr<rex::ui::SimpleSettingsDialog> simple_settings_dialog_;
std::unique_ptr<rex::ui::UltrawideTargetsDialog> ultrawide_targets_dialog_;
bool recipe_overlay_installed_ = false; bool recipe_overlay_installed_ = false;
bool big_device_aliases_installed_ = false; bool big_device_aliases_installed_ = false;
std::atomic<uint32_t> debug_marker_count_{0}; std::atomic<uint32_t> debug_marker_count_{0};

72
src/skate3_fov.cpp Normal file
View file

@ -0,0 +1,72 @@
#include "skate3_fov.h"
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstdint>
#include <string_view>
#include <rex/cvar.h>
namespace {
constexpr double kDefaultFieldOfViewDegrees = 60.0;
constexpr double kFieldOfViewEpsilon = 0.001;
constexpr double kDegreesToRadians = 3.14159265358979323846 / 180.0;
std::atomic<uint32_t> g_projection_fov_override_bits{0};
float FloatFromBits(uint32_t value) {
union {
uint32_t u;
float f;
} bits{value};
return bits.f;
}
uint32_t BitsFromFloat(float value) {
union {
float f;
uint32_t u;
} bits{value};
return bits.u;
}
} // namespace
void Skate3UpdateFieldOfViewOverride(double degrees) {
if (!std::isfinite(degrees) ||
std::abs(degrees - kDefaultFieldOfViewDegrees) <= kFieldOfViewEpsilon) {
g_projection_fov_override_bits.store(0, std::memory_order_relaxed);
return;
}
const double clamped_degrees = std::clamp(degrees, 40.0, 120.0);
const float radians = float(clamped_degrees * kDegreesToRadians);
if (!std::isfinite(radians) || radians <= 0.0f) {
g_projection_fov_override_bits.store(0, std::memory_order_relaxed);
return;
}
g_projection_fov_override_bits.store(BitsFromFloat(radians), std::memory_order_relaxed);
}
void Skate3InitializeFieldOfViewOverride() {
Skate3UpdateFieldOfViewOverride(rex::cvar::Query<double>("skate3_field_of_view"));
rex::cvar::RegisterChangeCallback(
"skate3_field_of_view", [](std::string_view, std::string_view value) {
double degrees = 0.0;
if (rex::cvar::ParseDouble(value, degrees)) {
Skate3UpdateFieldOfViewOverride(degrees);
}
});
}
float Skate3MaybeOverrideProjectionFovRadians(float native_radians) {
const uint32_t override_bits = g_projection_fov_override_bits.load(std::memory_order_relaxed);
if (!override_bits || !std::isfinite(native_radians) || native_radians < 0.5f ||
native_radians > 2.4f) {
return native_radians;
}
return FloatFromBits(override_bits);
}

5
src/skate3_fov.h Normal file
View file

@ -0,0 +1,5 @@
#pragma once
void Skate3InitializeFieldOfViewOverride();
void Skate3UpdateFieldOfViewOverride(double degrees);
float Skate3MaybeOverrideProjectionFovRadians(float native_radians);

View file

@ -0,0 +1,614 @@
#pragma once
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstdlib>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <unordered_map>
#include <string>
#include <string_view>
#include <mutex>
#include <rex/cvar.h>
#include <rex/graphics/ultrawide_debug.h>
#include <rex/logging.h>
inline bool Skate3UltrawideHasExpandedTargetAspect() {
if (!rex::cvar::Query<bool>("skate3_ultrawide") ||
!rex::cvar::Query<bool>("skate3_ultrawide_hor_plus") ||
!rex::graphics::ultrawide_debug::IsSkate3GameplayUltrawideActive()) {
return false;
}
return rex::cvar::Query<double>("skate3_ultrawide_target_aspect") >
(16.0 / 9.0) + 0.01;
}
inline float Skate3UltrawideFloatFromU32(uint32_t value) {
union {
uint32_t u;
float f;
} bits{value};
return bits.f;
}
inline uint32_t Skate3UltrawideU32FromFloat(float value) {
union {
float f;
uint32_t u;
} bits{value};
return bits.u;
}
inline double Skate3UltrawideMaybeOverrideGameplayFrustumAspect(uint32_t site_id,
double native_aspect) {
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags") ||
!std::isfinite(native_aspect) || native_aspect <= 0.0) {
return native_aspect;
}
const double forced_aspect = std::clamp(
rex::cvar::Query<double>("skate3_ultrawide_target_aspect"), 16.0 / 9.0, 8.0);
if (forced_aspect <= native_aspect + 0.001) {
return native_aspect;
}
const uint32_t native_milli = uint32_t(std::clamp(native_aspect * 1000.0, 0.0, 100000.0));
const uint32_t forced_milli = uint32_t(std::clamp(forced_aspect * 1000.0, 0.0, 100000.0));
rex::graphics::ultrawide_debug::RecordScreenCallback(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kWidth, site_id, native_milli);
rex::graphics::ultrawide_debug::RecordScreenCallbackResult(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kWidth, site_id, native_milli,
forced_milli);
return forced_aspect;
}
inline void Skate3UltrawideMaybeExpandMainCullRange(PPCContext& ctx, u8* base,
uint32_t min_addr,
uint32_t max_addr) {
if (!Skate3UltrawideHasExpandedTargetAspect()) {
return;
}
const double target_aspect = std::clamp(
rex::cvar::Query<double>("skate3_ultrawide_target_aspect"), 16.0 / 9.0, 8.0);
const float scale = float(target_aspect / (16.0 / 9.0));
if (scale <= 1.0f) {
return;
}
const float native_min = Skate3UltrawideFloatFromU32(REX_LOAD_U32(min_addr));
const float native_max = Skate3UltrawideFloatFromU32(REX_LOAD_U32(max_addr));
if (!std::isfinite(native_min) || !std::isfinite(native_max) ||
native_max <= native_min) {
return;
}
const float center = (native_min + native_max) * 0.5f;
const float half_width = (native_max - native_min) * 0.5f * scale;
const float widened_min = center - half_width;
const float widened_max = center + half_width;
if (!std::isfinite(widened_min) || !std::isfinite(widened_max)) {
return;
}
REX_STORE_U32(min_addr, Skate3UltrawideU32FromFloat(widened_min));
REX_STORE_U32(max_addr, Skate3UltrawideU32FromFloat(widened_max));
}
inline uint32_t Skate3UltrawideRecordScreenHeight(uint32_t site_id, uint32_t native_height) {
rex::graphics::ultrawide_debug::RecordScreenCallback(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kHeight, site_id, native_height);
return native_height;
}
inline uint32_t Skate3UltrawideMaybeOverrideScreenFlag(
rex::graphics::ultrawide_debug::ScreenCallbackKind kind, uint32_t site_id,
uint32_t native_value) {
const bool selected =
rex::graphics::ultrawide_debug::RecordScreenCallback(kind, site_id, native_value);
return selected ? 1 : native_value;
}
inline uint32_t Skate3UltrawideMaybeForceMainVisibilityFlag(uint32_t site_id,
uint32_t native_value) {
rex::graphics::ultrawide_debug::RecordScreenCallback(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value);
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags")) {
return native_value;
}
rex::graphics::ultrawide_debug::RecordScreenCallbackResult(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value, 1);
return 1;
}
inline uint32_t Skate3UltrawideMaybeForceHighestLodState(uint32_t site_id, uint32_t object,
uint32_t native_state) {
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags") ||
native_state == 0) {
return native_state;
}
return 0;
}
inline thread_local bool g_skate3_ultrawide_force_main_cull_classifier = false;
struct Skate3UltrawideMainCullClassifierScope {
explicit Skate3UltrawideMainCullClassifierScope(bool enabled)
: previous(g_skate3_ultrawide_force_main_cull_classifier) {
g_skate3_ultrawide_force_main_cull_classifier =
previous || (enabled && Skate3UltrawideHasExpandedTargetAspect());
}
~Skate3UltrawideMainCullClassifierScope() {
g_skate3_ultrawide_force_main_cull_classifier = previous;
}
bool previous;
};
inline uint32_t Skate3UltrawideMaybeForceMainCullClassifierHalfword(uint32_t native_value) {
if (!g_skate3_ultrawide_force_main_cull_classifier ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags")) {
return native_value;
}
return 1;
}
inline bool Skate3UltrawideShouldForceWorldMainCullDescriptor(u8* base, uint32_t descriptor) {
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags") ||
descriptor < 512) {
return false;
}
constexpr uint32_t kWorldVtable = 0x8230B278;
const uint32_t object = descriptor - 512;
if (REX_LOAD_U32(object) != kWorldVtable) {
return false;
}
const uint32_t output_flags = REX_LOAD_U32(descriptor + 12);
return output_flags != 0 && output_flags == REX_LOAD_U32(object + 496);
}
inline bool Skate3UltrawideShouldWidenGameCullFrustum(u8* base, uint32_t cull_block) {
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_widen_game_frustum") || !cull_block) {
return false;
}
return REX_LOAD_U32(cull_block + 5260) != 0;
}
struct Skate3UltrawideGameFrustumPatchScope {
Skate3UltrawideGameFrustumPatchScope(PPCContext& ctx_, u8* base_, uint32_t cull_block)
: ctx(ctx_), base(base_) {
if (!Skate3UltrawideShouldWidenGameCullFrustum(base, cull_block)) {
return;
}
const double target_aspect = std::clamp(
rex::cvar::Query<double>("skate3_ultrawide_target_aspect"), 16.0 / 9.0, 8.0);
side_scale = float((16.0 / 9.0) / target_aspect);
if (!(side_scale > 0.0f && side_scale < 0.999f)) {
return;
}
block = cull_block;
PlaneBits current_left_bits = LoadPlaneBits(kLeftPlaneOffset);
PlaneBits current_right_bits = LoadPlaneBits(kRightPlaneOffset);
WidenedPlaneCache& cache = CacheForBlock();
if (cache.valid && cache.side_scale == side_scale &&
current_left_bits == cache.widened[0] && current_right_bits == cache.widened[1]) {
return;
}
cache.original[0] = current_left_bits;
cache.original[1] = current_right_bits;
Plane left = LoadPlane(kLeftPlaneOffset);
Plane right = LoadPlane(kRightPlaneOffset);
Plane widened_left{};
Plane widened_right{};
for (size_t i = 0; i < left.size(); ++i) {
const float center = (left[i] + right[i]) * 0.5f;
const float side = (left[i] - right[i]) * 0.5f * side_scale;
widened_left[i] = center + side;
widened_right[i] = center - side;
}
if (!NormalizePlane(widened_left) || !NormalizePlane(widened_right)) {
return;
}
StorePlane(kLeftPlaneOffset, widened_left);
StorePlane(kRightPlaneOffset, widened_right);
cache.widened[0] = LoadPlaneBits(kLeftPlaneOffset);
cache.widened[1] = LoadPlaneBits(kRightPlaneOffset);
cache.side_scale = side_scale;
cache.valid = true;
active = true;
}
~Skate3UltrawideGameFrustumPatchScope() = default;
Skate3UltrawideGameFrustumPatchScope(const Skate3UltrawideGameFrustumPatchScope&) = delete;
Skate3UltrawideGameFrustumPatchScope& operator=(const Skate3UltrawideGameFrustumPatchScope&) =
delete;
private:
using Plane = std::array<float, 4>;
using PlaneBits = std::array<uint32_t, 4>;
struct WidenedPlaneCache {
uint32_t block = 0;
std::array<PlaneBits, 2> original{};
std::array<PlaneBits, 2> widened{};
float side_scale = 1.0f;
bool valid = false;
};
static constexpr uint32_t kLeftPlaneOffset = 5152;
static constexpr uint32_t kRightPlaneOffset = 5168;
float LoadFloat(uint32_t address) const {
PPCRegister value{};
value.u32 = REX_LOAD_U32(address);
return value.f32;
}
void StoreFloat(uint32_t address, float number) const {
PPCRegister value{};
value.f32 = number;
REX_STORE_U32(address, value.u32);
}
Plane LoadPlane(uint32_t offset) const {
Plane plane{};
for (uint32_t i = 0; i < plane.size(); ++i) {
plane[i] = LoadFloat(block + offset + i * 4);
}
return plane;
}
void StorePlane(uint32_t offset, const Plane& plane) const {
for (uint32_t i = 0; i < plane.size(); ++i) {
StoreFloat(block + offset + i * 4, plane[i]);
}
}
PlaneBits LoadPlaneBits(uint32_t offset) const {
PlaneBits plane{};
for (uint32_t i = 0; i < plane.size(); ++i) {
plane[i] = REX_LOAD_U32(block + offset + i * 4);
}
return plane;
}
WidenedPlaneCache& CacheForBlock() {
static thread_local std::array<WidenedPlaneCache, 16> cache_entries{};
WidenedPlaneCache* empty = nullptr;
for (WidenedPlaneCache& entry : cache_entries) {
if (entry.valid && entry.block == block) {
return entry;
}
if (!entry.valid && !empty) {
empty = &entry;
}
}
WidenedPlaneCache& entry = empty ? *empty : cache_entries[block % cache_entries.size()];
entry = {};
entry.block = block;
return entry;
}
static bool NormalizePlane(Plane& plane) {
const float length =
std::sqrt(plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2]);
if (!(length > 0.00001f) || !std::isfinite(length)) {
return false;
}
const float inv_length = 1.0f / length;
for (float& value : plane) {
value *= inv_length;
if (!std::isfinite(value)) {
return false;
}
}
return true;
}
uint32_t block = 0;
PPCContext& ctx;
u8* base = nullptr;
float side_scale = 1.0f;
bool active = false;
};
inline std::filesystem::path Skate3UltrawideCacheLogPath(const char* filename);
enum Skate3UltrawideObjectStageBits : uint32_t {
kSkate3ObjectStageActive = 1u << 0,
kSkate3ObjectStagePrep = 1u << 1,
kSkate3ObjectStageList = 1u << 2,
kSkate3ObjectStageMain = 1u << 3,
};
struct Skate3UltrawideObjectStageRecord {
uint32_t bits = 0;
uint32_t vtable = 0;
uint32_t target = 0;
uint32_t first_site = 0;
uint32_t last_site = 0;
uint32_t aux0 = 0;
uint32_t aux1 = 0;
};
inline uint64_t& Skate3UltrawideObjectStageFrameId() {
static uint64_t frame = 0;
return frame;
}
inline uint32_t& Skate3UltrawideObjectStageRenderer() {
static uint32_t renderer = 0;
return renderer;
}
inline std::unordered_map<uint32_t, Skate3UltrawideObjectStageRecord>&
Skate3UltrawideObjectStageRecords() {
static std::unordered_map<uint32_t, Skate3UltrawideObjectStageRecord> records;
return records;
}
inline std::mutex& Skate3UltrawideObjectStageMutex() {
static std::mutex mutex;
return mutex;
}
inline std::ofstream& Skate3UltrawideObjectStageLogFile() {
static std::ofstream log_file;
if (!log_file.is_open()) {
std::filesystem::path log_path =
Skate3UltrawideCacheLogPath("skate3_ultrawide_object_stage_trace.log");
std::error_code ec;
if (log_path.has_parent_path()) {
std::filesystem::create_directories(log_path.parent_path(), ec);
}
log_file.open(log_path, std::ios::out | std::ios::trunc);
if (log_file.is_open()) {
log_file << "seq,frame,kind,marker,renderer,object,bits,site,vtable,target,aux0,aux1,"
"active,prep,list,main,missing_main,total\n";
}
}
return log_file;
}
inline bool Skate3UltrawideObjectStageTraceEnabled() {
return rex::cvar::Query<bool>("skate3_ultrawide_object_stage_trace_continuous") ||
rex::cvar::Query<int32_t>("skate3_ultrawide_object_stage_trace_frames_remaining") > 0 ||
rex::cvar::Query<int32_t>("skate3_ultrawide_object_stage_marker") != 0;
}
inline void Skate3UltrawideWriteObjectStageLog(
uint64_t frame, const char* kind, uint32_t marker, uint32_t renderer, uint32_t object,
uint32_t bits, uint32_t site, uint32_t vtable, uint32_t target, uint32_t aux0,
uint32_t aux1, uint32_t active_count, uint32_t prep_count, uint32_t list_count,
uint32_t main_count, uint32_t missing_main_count, uint32_t total_count) {
if (!rex::cvar::Query<bool>("skate3_ultrawide_object_stage_trace_to_disk")) {
return;
}
static std::atomic<uint64_t> sequence{0};
std::ofstream& log_file = Skate3UltrawideObjectStageLogFile();
if (!log_file.is_open()) {
return;
}
log_file << std::dec << sequence.fetch_add(1, std::memory_order_relaxed) << ',' << frame
<< ',' << kind << ',' << marker << ",0x" << std::hex << renderer << ",0x"
<< object << ",0x" << bits << ",0x" << site << ",0x" << vtable << ",0x"
<< target << ",0x" << aux0 << ",0x" << aux1 << ',' << std::dec << active_count
<< ',' << prep_count << ',' << list_count << ',' << main_count << ','
<< missing_main_count << ',' << total_count << '\n';
log_file.flush();
}
inline void Skate3UltrawideFinalizeObjectStageFrameLocked(const char* reason) {
auto& records = Skate3UltrawideObjectStageRecords();
if (records.empty()) {
return;
}
const uint32_t marker =
static_cast<uint32_t>(rex::cvar::Query<int32_t>("skate3_ultrawide_object_stage_marker"));
const bool enabled = Skate3UltrawideObjectStageTraceEnabled();
uint32_t remaining =
static_cast<uint32_t>(std::max(0, rex::cvar::Query<int32_t>(
"skate3_ultrawide_object_stage_trace_frames_remaining")));
if (!enabled) {
records.clear();
return;
}
uint32_t active_count = 0;
uint32_t prep_count = 0;
uint32_t list_count = 0;
uint32_t main_count = 0;
uint32_t missing_main_count = 0;
for (const auto& [object, record] : records) {
active_count += (record.bits & kSkate3ObjectStageActive) ? 1 : 0;
prep_count += (record.bits & kSkate3ObjectStagePrep) ? 1 : 0;
list_count += (record.bits & kSkate3ObjectStageList) ? 1 : 0;
main_count += (record.bits & kSkate3ObjectStageMain) ? 1 : 0;
if ((record.bits & (kSkate3ObjectStagePrep | kSkate3ObjectStageList)) &&
!(record.bits & kSkate3ObjectStageMain)) {
++missing_main_count;
}
}
const uint64_t frame = Skate3UltrawideObjectStageFrameId();
const uint32_t renderer = Skate3UltrawideObjectStageRenderer();
Skate3UltrawideWriteObjectStageLog(frame, reason, marker, renderer, 0, 0, 0, 0, 0, 0, 0,
active_count, prep_count, list_count, main_count,
missing_main_count, static_cast<uint32_t>(records.size()));
uint32_t emitted_missing = 0;
const uint32_t missing_limit = static_cast<uint32_t>(std::clamp(
rex::cvar::Query<int32_t>("skate3_ultrawide_object_stage_missing_limit"), 0, 4096));
for (const auto& [object, record] : records) {
if (!((record.bits & (kSkate3ObjectStagePrep | kSkate3ObjectStageList)) &&
!(record.bits & kSkate3ObjectStageMain))) {
continue;
}
if (emitted_missing++ >= missing_limit) {
break;
}
Skate3UltrawideWriteObjectStageLog(frame, "missing-main", marker, renderer, object,
record.bits, record.last_site, record.vtable,
record.target, record.aux0, record.aux1, active_count,
prep_count, list_count, main_count, missing_main_count,
static_cast<uint32_t>(records.size()));
}
if (remaining > 0) {
rex::cvar::SetFlagByName("skate3_ultrawide_object_stage_trace_frames_remaining",
std::to_string(remaining - 1));
}
if (marker != 0) {
rex::cvar::SetFlagByName("skate3_ultrawide_object_stage_marker", "0");
}
records.clear();
}
inline void Skate3UltrawideBeginObjectStageFrame(uint32_t site_id, uint32_t renderer) {
if (!Skate3UltrawideHasExpandedTargetAspect()) {
return;
}
if (renderer < 0x10000) {
return;
}
std::lock_guard lock(Skate3UltrawideObjectStageMutex());
Skate3UltrawideFinalizeObjectStageFrameLocked("frame");
++Skate3UltrawideObjectStageFrameId();
Skate3UltrawideObjectStageRenderer() = renderer;
if (Skate3UltrawideObjectStageTraceEnabled()) {
Skate3UltrawideWriteObjectStageLog(Skate3UltrawideObjectStageFrameId(), "begin", 0,
renderer, 0, 0, site_id, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
}
inline uint32_t Skate3UltrawideObjectStageBitForAction(std::string_view action) {
if (action == "render-active-v68") {
return kSkate3ObjectStageActive;
}
if (action == "render-prep-v48") {
return kSkate3ObjectStagePrep;
}
if (action == "render-list-v24") {
return kSkate3ObjectStageList;
}
if (action == "render-main-v52") {
return kSkate3ObjectStageMain;
}
return 0;
}
inline void Skate3UltrawideRecordObjectStage(const char* action, uint32_t site_id,
uint32_t object, uint32_t vtable,
uint32_t target, uint32_t aux0,
uint32_t aux1, uint32_t renderer) {
const uint32_t bit = Skate3UltrawideObjectStageBitForAction(action ? action : "");
if (!bit || !object || !Skate3UltrawideHasExpandedTargetAspect()) {
return;
}
std::lock_guard lock(Skate3UltrawideObjectStageMutex());
auto& record = Skate3UltrawideObjectStageRecords()[object];
if (!record.bits) {
record.first_site = site_id;
record.vtable = vtable;
record.target = target;
}
record.bits |= bit;
record.last_site = site_id;
record.aux0 = aux0;
record.aux1 = aux1;
if (vtable) {
record.vtable = vtable;
}
if (target) {
record.target = target;
}
if (renderer >= 0x10000) {
Skate3UltrawideObjectStageRenderer() = renderer;
}
}
inline uint32_t Skate3UltrawideMaybeForceMainObjectActive(uint32_t site_id,
uint32_t native_value) {
rex::graphics::ultrawide_debug::RecordScreenCallback(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value);
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags")) {
return native_value;
}
rex::graphics::ultrawide_debug::RecordScreenCallbackResult(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value, 1);
return 1;
}
inline uint32_t Skate3UltrawideMaybeForceWorldPartVisible(uint32_t site_id,
uint32_t native_value,
uint32_t sibling_value) {
rex::graphics::ultrawide_debug::RecordScreenCallback(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value);
if (native_value != 0 || sibling_value == 0 || !Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags")) {
return native_value;
}
rex::graphics::ultrawide_debug::RecordScreenCallbackResult(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value, 1);
return 1;
}
inline std::filesystem::path Skate3UltrawideCacheLogPath(const char* filename) {
#if defined(_WIN32)
char* appdata_raw = nullptr;
size_t appdata_length = 0;
if (_dupenv_s(&appdata_raw, &appdata_length, "APPDATA") == 0 && appdata_raw &&
appdata_length > 0) {
std::filesystem::path appdata_path(appdata_raw);
std::free(appdata_raw);
return appdata_path / "skate3" / "cache" / filename;
}
std::free(appdata_raw);
#else
if (const char* appdata = std::getenv("APPDATA")) {
return std::filesystem::path(appdata) / "skate3" / "cache" / filename;
}
#endif
return filename;
}
inline void Skate3UltrawideTraceRenderVirtual(const char* action, uint32_t site_id,
uint32_t object_ptr, uint32_t vtable_ptr,
uint32_t target_ptr, uint32_t aux0 = 0,
uint32_t aux1 = 0, uint32_t renderer = 0) {
Skate3UltrawideRecordObjectStage(action, site_id, object_ptr, vtable_ptr, target_ptr, aux0,
aux1, renderer);
}

@ -1 +1 @@
Subproject commit 34accdd2419240d25f7fc9bfe8870886613d39d3 Subproject commit 05049e0769ff49c928beaa6414d225715876c978