in-world neon spline waypoints, F5 live renderer toggle, F6 screenshots, compass icon fixes

This commit is contained in:
mchughalex 2026-07-05 01:38:10 +01:00
parent d101dc8b78
commit 70812600f7
9 changed files with 1530 additions and 47 deletions

View file

@ -229,6 +229,8 @@ set(SKATE3_COMMON_SOURCES
src/skate3_iso_installer.cpp
src/skate3_native_render.cpp
src/skate3_native_scene.cpp
src/skate3_screenshot.cpp
src/skate3_shader_disasm.cpp
src/skate3_title_update_installer.cpp
src/skate3_user_settings.cpp
third_party/rexglue-sdk/thirdparty/crypto/sha256.cpp
@ -295,7 +297,7 @@ if(APPLE)
endif()
if(WIN32)
target_link_libraries(skate3 PRIVATE comdlg32 winhttp d3dcompiler)
target_link_libraries(skate3 PRIVATE comdlg32 winhttp d3dcompiler gdiplus)
endif()
add_custom_target(skate3_stage_runtime_library

View file

@ -4,6 +4,9 @@
#include "skate3_fov.h"
#include "skate3_iso_installer.h"
#include "skate3_native_render.h"
#include "skate3_native_scene.h"
#include "skate3_screenshot.h"
#include "skate3_shader_disasm.h"
#include "skate3_title_update_installer.h"
#include "skate3_user_settings.h"
@ -60,6 +63,7 @@
#include <rex/system.h>
#include <rex/ui/flags.h>
#include <rex/ui/keybinds.h>
#include <rex/ui/window.h>
#include <rex/ui/overlay/simple_settings_overlay.h>
#include <rex/ui/overlay/ultrawide_targets_overlay.h>
@ -695,6 +699,15 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) {
rex::ui::RegisterBind("bind_skate3_menu_alt", "F1", "Skate 3 settings alternate", [this] {
ToggleSimpleSettings();
});
rex::ui::RegisterBind("bind_skate3_screenshot", "F6",
"Save screenshot to screenshots/", [this] {
skate3::screenshot::CaptureWindow(
window() ? window()->GetNativeWindowHandle() : nullptr);
});
rex::ui::RegisterBind("bind_skate3_native_render_toggle", "F5",
"Toggle native/emulated renderer", [] {
skate3::native_scene::ToggleSceneEnabled();
});
rex::ui::RegisterBind("bind_skate3_ultrawide_targets", "F7",
"Skate 3 ultrawide targets", [this] {
ToggleUltrawideTargets();
@ -714,6 +727,7 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) {
}
void Skate3BaseApp::OnPostSetup() {
skate3::shader_disasm::RunIfRequested();
ApplySelectedProfileToRuntime();
ApplyGameplayCursorMode();

View file

@ -502,6 +502,117 @@ extern "C" REX_FUNC(sub_82B7A458) {
}
}
// ---- 2D / APT (Flash-converted HUD) reconnaissance hooks -----------------
// Every HUD/menu 2D element is a Flash SWF converted to EA APT, rendered by
// Sk8::FE::AptRenderingIntegration through the same guest D3D draw functions
// hooked above. These brackets tag draws issued inside the 2D pass so the
// recorder can capture and the future native 2D pass can replay them.
// Sk8::FE::FrontEndManager::Render2D(unsigned int): the game's whole 2D
// pass (FE movies + HUD).
extern "C" REX_FUNC(sub_825D9168) {
const bool enabled = skate3::native_render::Enabled();
if (enabled) skate3::native_scene::On2dPhase(0, true);
__imp__sub_825D9168(ctx, base);
if (enabled) skate3::native_scene::On2dPhase(0, false);
}
// Sk8::FE::AptMovieIntegration::Render(unsigned int): one APT movie.
extern "C" REX_FUNC(sub_825D67D8) {
const bool enabled = skate3::native_render::Enabled();
if (enabled) skate3::native_scene::On2dPhase(1, true);
__imp__sub_825D67D8(ctx, base);
if (enabled) skate3::native_scene::On2dPhase(1, false);
}
// Sk8::FE::AptRenderingIntegration::DrawRenderingUnit(void*, AptRenderInfo
// const*): one APT display-list element (texture quad / vector shape).
extern "C" REX_FUNC(sub_825D4490) {
const bool enabled = skate3::native_render::Enabled();
if (enabled) skate3::native_scene::On2dPhase(2, true);
__imp__sub_825D4490(ctx, base);
if (enabled) skate3::native_scene::On2dPhase(2, false);
}
// Sk8::FE::AptRenderingIntegration::UpdateRenderToTexture(unsigned int):
// in gameplay this renders the whole HUD into a screen-sized overlay
// texture at true screen coordinates (the game composites it later through
// the suppressed emulated pass). Diagnostic bracket only.
extern "C" REX_FUNC(sub_825D4E50) {
const bool enabled = skate3::native_render::Enabled();
if (enabled) skate3::native_scene::On2dPhase(3, true);
__imp__sub_825D4E50(ctx, base);
if (enabled) skate3::native_scene::On2dPhase(3, false);
}
// Sk8::Render::cFont::DrawstringLocal<char> / <unsigned short>: the glyph
// text emitter (trick names, scores). Text can flush OUTSIDE the APT
// brackets, so it gets its own bit.
extern "C" REX_FUNC(sub_82808388) {
const bool enabled = skate3::native_render::Enabled();
if (enabled) skate3::native_scene::On2dPhase(4, true);
__imp__sub_82808388(ctx, base);
if (enabled) skate3::native_scene::On2dPhase(4, false);
}
extern "C" REX_FUNC(sub_82808708) {
const bool enabled = skate3::native_render::Enabled();
if (enabled) skate3::native_scene::On2dPhase(4, true);
__imp__sub_82808708(ctx, base);
if (enabled) skate3::native_scene::On2dPhase(4, false);
}
// Sk8::Render::SimpleDraw::DrawParameters::Draw: the game's immediate-mode
// quad/tri utility (chase arrows, in-world guide markers/beams, debug
// draws). Bottoms out in BeginVertices like the APT path.
extern "C" REX_FUNC(sub_82804168) {
const bool enabled = skate3::native_render::Enabled();
if (enabled) skate3::native_scene::On2dPhase(5, true);
__imp__sub_82804168(ctx, base);
if (enabled) skate3::native_scene::On2dPhase(5, false);
}
// D3DDevice_SetPixelShader / SetVertexShader (r4 = guest shader object):
// recorded per draw to group the 2D stream by shader variant.
extern "C" REX_FUNC(sub_82B7F408) {
if (skate3::native_render::Enabled()) {
skate3::native_scene::OnSetShader(true, ctx.r4.u32);
}
__imp__sub_82B7F408(ctx, base);
}
extern "C" REX_FUNC(sub_82B7F150) {
if (skate3::native_render::Enabled()) {
skate3::native_scene::OnSetShader(false, ctx.r4.u32);
}
__imp__sub_82B7F150(ctx, base);
}
// D3D::SetPending_RenderStates(device, u64 dirty mask, bank, ptr): the
// render-state shadow bank (blend/depth state for 2D draws lives here).
extern "C" REX_FUNC(sub_82B83C48) {
if (skate3::native_render::Enabled()) {
skate3::native_scene::OnRenderStateUpload(ctx.r4.u64, ctx.r5.u32, ctx.r6.u32);
}
__imp__sub_82B83C48(ctx, base);
}
// D3DDevice_SetViewport(device, D3DVIEWPORT*) / SetScissorRect(device,
// RECT*): recorded per draw (render-to-texture APT passes and mask rects).
extern "C" REX_FUNC(sub_82B74310) {
if (skate3::native_render::Enabled()) {
skate3::native_scene::OnSetViewport(base, ctx.r4.u32);
}
__imp__sub_82B74310(ctx, base);
}
extern "C" REX_FUNC(sub_82B769C0) {
if (skate3::native_render::Enabled()) {
skate3::native_scene::OnSetScissor(base, ctx.r4.u32);
}
__imp__sub_82B769C0(ctx, base);
}
// D3DDevice_BeginVertices: inline (write-through-ring) vertex path; the
// CPU writes computed vertices directly. Post-call r3 = guest write pointer.
extern "C" REX_FUNC(sub_82B79FC0) {

File diff suppressed because it is too large Load diff

View file

@ -100,6 +100,35 @@ void OnRegisterTexture(uint64_t guid, uint32_t texture);
void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t ptr,
uint32_t device);
// 2D/APT phase bracket (the HUD and every other 2D element is a Flash SWF
// converted to EA APT, rendered through FE::AptRenderingIntegration). Guest
// draws issued while inside a bracket are 2D draws. bit: 0 =
// FrontEndManager::Render2D, 1 = AptMovieIntegration::Render, 2 =
// AptRenderingIntegration::DrawRenderingUnit, 3 =
// AptRenderingIntegration::UpdateRenderToTexture (in gameplay the whole HUD
// renders inside bit 3 into a screen-sized overlay texture at true screen
// coordinates; replaying those draws directly is the composite), 4 =
// Sk8::Render::cFont::DrawstringLocal (glyph text: trick names/scores;
// text can flush outside the APT brackets), 5 =
// Sk8::Render::SimpleDraw::DrawParameters::Draw (immediate-mode quads:
// chase arrows, in-world markers/beams).
void On2dPhase(uint32_t bit, bool enter);
// Current guest shader objects (D3DDevice_SetPixelShader/SetVertexShader),
// recorded per draw so offline analysis can group the 2D draw stream by
// shader variant.
void OnSetShader(bool pixel, uint32_t obj);
// D3D::SetPending_RenderStates(device, mask, bank, ptr): the render-state
// shadow bank (blend/depth state for the 2D pass lives here).
void OnRenderStateUpload(uint64_t mask, uint32_t bank, uint32_t ptr);
// D3DDevice_SetViewport / SetRenderState_ScissorTestEnable-adjacent scissor
// tracking, recorded per draw (render-to-texture APT passes show up as
// non-fullscreen viewports).
void OnSetViewport(uint8_t* base, uint32_t viewport_ptr);
void OnSetScissor(uint8_t* base, uint32_t rect_ptr);
// Guest buffer-binding trackers + post-draw state fixup: each
// DrawIndexedVertices whose bound IB/VB match a pending item fills that
// item's bone palette / world matrix from the constant bank (FIFO one-shot
@ -154,4 +183,12 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count);
// at startup regardless of cvar state.
void Install();
// Runtime renderer switch (bound to F5): flips skate3_native_render_scene
// live. Capture hooks, scene build, guest-output replacement and
// emulated-draw suppression all re-check the cvar every frame, so the swap
// between the native renderer and the emulated GPU is seamless. Returns the
// new state; refuses (returns false) when the skate3_native_render hook
// layer was not enabled at boot.
bool ToggleSceneEnabled();
} // namespace skate3::native_scene

158
src/skate3_screenshot.cpp Normal file
View file

@ -0,0 +1,158 @@
#include "skate3_screenshot.h"
#include <rex/logging.h>
#if defined(_WIN32)
#include <windows.h>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <ctime>
#include <filesystem>
#include <memory>
#include <string>
#include <thread>
// gdiplus.h uses unqualified min/max, which NOMINMAX removes.
namespace Gdiplus {
using std::max;
using std::min;
} // namespace Gdiplus
#include <objidl.h>
#include <gdiplus.h>
#ifndef PW_RENDERFULLCONTENT
#define PW_RENDERFULLCONTENT 0x00000002
#endif
namespace skate3::screenshot {
namespace {
bool EnsureGdiplus() {
static bool initialized = [] {
Gdiplus::GdiplusStartupInput input;
ULONG_PTR token = 0;
return Gdiplus::GdiplusStartup(&token, &input, nullptr) == Gdiplus::Ok;
}();
return initialized;
}
bool PngEncoderClsid(CLSID* clsid) {
UINT count = 0;
UINT bytes = 0;
if (Gdiplus::GetImageEncodersSize(&count, &bytes) != Gdiplus::Ok || bytes == 0) {
return false;
}
std::string storage(bytes, '\0');
auto* codecs = reinterpret_cast<Gdiplus::ImageCodecInfo*>(storage.data());
if (Gdiplus::GetImageEncoders(count, bytes, codecs) != Gdiplus::Ok) {
return false;
}
for (UINT i = 0; i < count; ++i) {
if (wcscmp(codecs[i].MimeType, L"image/png") == 0) {
*clsid = codecs[i].Clsid;
return true;
}
}
return false;
}
std::filesystem::path ScreenshotPath() {
const auto now = std::chrono::system_clock::now();
const std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm{};
localtime_s(&tm, &t);
char name[64];
std::snprintf(name, sizeof(name), "shot_%04d%02d%02d_%02d%02d%02d.png",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min,
tm.tm_sec);
return std::filesystem::absolute(std::filesystem::path("screenshots") / name);
}
void EncodeAndSave(HBITMAP bitmap, int crop_x, int crop_y, int crop_w, int crop_h,
std::filesystem::path path) {
if (!EnsureGdiplus()) {
REXLOG_ERROR("screenshot: GDI+ startup failed");
DeleteObject(bitmap);
return;
}
CLSID png;
if (!PngEncoderClsid(&png)) {
REXLOG_ERROR("screenshot: no PNG encoder");
DeleteObject(bitmap);
return;
}
Gdiplus::Bitmap full(bitmap, nullptr);
std::unique_ptr<Gdiplus::Bitmap> crop(
full.Clone(crop_x, crop_y, crop_w, crop_h, PixelFormat32bppRGB));
DeleteObject(bitmap);
if (!crop || crop->GetLastStatus() != Gdiplus::Ok) {
REXLOG_ERROR("screenshot: client-area crop failed");
return;
}
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (crop->Save(path.wstring().c_str(), &png, nullptr) != Gdiplus::Ok) {
REXLOG_ERROR("screenshot: PNG save failed for {}", path.string());
return;
}
REXLOG_INFO("screenshot: saved {} ({}x{})", path.string(), crop_w, crop_h);
}
} // namespace
void CaptureWindow(void* window_handle) {
HWND hwnd = static_cast<HWND>(window_handle);
if (hwnd == nullptr) {
REXLOG_WARN("screenshot: no window handle");
return;
}
RECT wr{};
RECT cr{};
if (!GetWindowRect(hwnd, &wr) || !GetClientRect(hwnd, &cr)) {
REXLOG_WARN("screenshot: window rect query failed");
return;
}
const int win_w = wr.right - wr.left;
const int win_h = wr.bottom - wr.top;
const int crop_w = cr.right - cr.left;
const int crop_h = cr.bottom - cr.top;
if (win_w <= 0 || win_h <= 0 || crop_w <= 0 || crop_h <= 0) {
REXLOG_WARN("screenshot: window is zero-sized (minimized?)");
return;
}
POINT client_origin{0, 0};
ClientToScreen(hwnd, &client_origin);
HDC screen_dc = GetDC(nullptr);
HDC mem_dc = CreateCompatibleDC(screen_dc);
HBITMAP bitmap = CreateCompatibleBitmap(screen_dc, win_w, win_h);
HGDIOBJ old = SelectObject(mem_dc, bitmap);
const BOOL printed = PrintWindow(hwnd, mem_dc, PW_RENDERFULLCONTENT);
SelectObject(mem_dc, old);
DeleteDC(mem_dc);
ReleaseDC(nullptr, screen_dc);
if (!printed) {
DeleteObject(bitmap);
REXLOG_WARN("screenshot: PrintWindow failed");
return;
}
std::thread(EncodeAndSave, bitmap, int(client_origin.x - wr.left),
int(client_origin.y - wr.top), crop_w, crop_h, ScreenshotPath())
.detach();
}
} // namespace skate3::screenshot
#else // !_WIN32
namespace skate3::screenshot {
void CaptureWindow(void* /*window_handle*/) {
REXLOG_WARN("screenshot: only implemented on Windows");
}
} // namespace skate3::screenshot
#endif // _WIN32

14
src/skate3_screenshot.h Normal file
View file

@ -0,0 +1,14 @@
#pragma once
namespace skate3::screenshot {
// Captures the game window (F6) and writes a timestamped PNG to
// screenshots/ under the working directory (the install dir), logging the
// absolute path. Uses PrintWindow PW_RENDERFULLCONTENT, the flip-model
// swapchain reads black through a plain GDI BitBlt, and crops to the client
// area. The capture runs on the calling (UI) thread; the PNG encode runs on
// a detached worker so the frame hitch stays minimal. Windows only; a no-op
// with a warning elsewhere. hwnd is the platform-native window handle.
void CaptureWindow(void* hwnd);
} // namespace skate3::screenshot

View file

@ -0,0 +1,71 @@
// Dev tool: offline Xenos ucode disassembly through the SDK's shader
// analyzer. When skate3_disasm_ucode_dir is set, every *.bin in that
// directory is treated as raw big-endian microcode (filename prefix vs_/ps_
// selects the shader type) and a .txt disassembly is written next to it at
// startup. Used to decode captured guest shaders (e.g. the spline/beam
// renderer) when porting draw paths to the native renderer.
#include "skate3_shader_disasm.h"
#include <bit>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include <rex/cvar.h>
#include <rex/graphics/pipeline/shader/shader.h>
#include <rex/graphics/xenos.h>
#include <rex/logging.h>
#include <rex/string/buffer.h>
REXCVAR_DEFINE_STRING(skate3_disasm_ucode_dir, "", "Skate 3",
"Dev: disassemble every *.bin (raw big-endian Xenos microcode, "
"vs_/ps_ filename prefix = shader type) in this directory at "
"startup, writing <name>.txt beside each file")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
namespace skate3::shader_disasm {
void RunIfRequested() {
const std::string dir_str(REXCVAR_GET(skate3_disasm_ucode_dir));
if (dir_str.empty()) {
return;
}
const std::filesystem::path dir(dir_str);
std::error_code ec;
if (!std::filesystem::is_directory(dir, ec)) {
REXLOG_WARN("shader-disasm: {} is not a directory", dir_str);
return;
}
rex::string::StringBuffer disasm_buffer;
for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) {
if (entry.path().extension() != ".bin") {
continue;
}
const std::string name = entry.path().filename().string();
const bool pixel = name.rfind("ps_", 0) == 0;
std::ifstream in(entry.path(), std::ios::binary);
std::vector<char> bytes((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
if (bytes.size() < 8 || (bytes.size() % 4) != 0) {
REXLOG_WARN("shader-disasm: {} skipped (size {})", name, bytes.size());
continue;
}
rex::graphics::Shader shader(
pixel ? rex::graphics::xenos::ShaderType::kPixel
: rex::graphics::xenos::ShaderType::kVertex,
0, reinterpret_cast<const uint32_t*>(bytes.data()), bytes.size() / 4,
std::endian::big);
shader.AnalyzeUcode(disasm_buffer);
std::filesystem::path out_path = entry.path();
out_path += ".txt";
std::ofstream out(out_path);
out << shader.ucode_disassembly();
REXLOG_INFO("shader-disasm: {} -> {} ({} bytes of disassembly)", name,
out_path.filename().string(), shader.ucode_disassembly().size());
}
}
} // namespace skate3::shader_disasm

View file

@ -0,0 +1,9 @@
#pragma once
namespace skate3::shader_disasm {
// If skate3_disasm_ucode_dir is set, disassembles every raw microcode *.bin
// in that directory to <name>.bin.txt. Call once at startup; no-op otherwise.
void RunIfRequested();
} // namespace skate3::shader_disasm